Skip to content

Commit 557d31e

Browse files
jimmodpgeorge
authored andcommitted
py/objint: Try to convert big-int back to small-int after binary op.
Before this change, long/mpz ints propagated into all future calculations, even if their value could fit in a small-int object. With this change, the result of a big-int binary op will now be converted to a small-int object if the value fits in a small-int. For example, a relatively common operation like `x = a * b // c` where a,b,c all small ints would always result in a long/mpz int, even if it didn't need to, and then this would impact all future calculations with x. This adds +24 bytes on PYBV11 but avoids heap allocations and potential surprises (e.g. `big-big` is now a small `0`, and can safely be accessed with MP_OBJ_SMALL_INT_VALUE). Performance tests are unchanged on PYBV10, except for `bm_pidigits.py` which makes heavy use of big-ints and gains about 8% in speed. Unix coverage tests have been updated to cover mpz code that is now unreachable by normal Python code (removing the unreachable code would lead to some surprising gaps in the internal C functions and the functionality may be needed in the future, so it is kept because it has minimal overhead). This work was funded through GitHub Sponsors. Signed-off-by: Jim Mussared <[email protected]>
1 parent 0600e4f commit 557d31e

File tree

6 files changed

+80
-15
lines changed

6 files changed

+80
-15
lines changed

ports/unix/coverage.c

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#include "py/obj.h"
66
#include "py/objfun.h"
7+
#include "py/objint.h"
78
#include "py/objstr.h"
89
#include "py/runtime.h"
910
#include "py/gc.h"
@@ -454,6 +455,13 @@ static mp_obj_t extra_coverage(void) {
454455
mpz_mul_inpl(&mpz, &mpz2, &mpz);
455456
mpz_as_uint_checked(&mpz, &value);
456457
mp_printf(&mp_plat_print, "%d\n", (int)value);
458+
459+
// mpz_not_inpl with argument==0, testing ~0
460+
mpz_set_from_int(&mpz, 0);
461+
mpz_not_inpl(&mpz, &mpz);
462+
mp_int_t value_signed;
463+
mpz_as_int_checked(&mpz, &value_signed);
464+
mp_printf(&mp_plat_print, "%d\n", (int)value_signed);
457465
}
458466

459467
// runtime utils
@@ -470,6 +478,9 @@ static mp_obj_t extra_coverage(void) {
470478
// call mp_call_function_2_protected with invalid args
471479
mp_call_function_2_protected(MP_OBJ_FROM_PTR(&mp_builtin_divmod_obj), mp_obj_new_str("abc", 3), mp_obj_new_str("abc", 3));
472480

481+
// mp_obj_int_get_checked with mp_obj_int_t that has a value that is a small integer
482+
mp_printf(&mp_plat_print, "%d\n", mp_obj_int_get_checked(mp_obj_int_new_mpz()));
483+
473484
// mp_obj_int_get_uint_checked with non-negative small-int
474485
mp_printf(&mp_plat_print, "%d\n", (int)mp_obj_int_get_uint_checked(MP_OBJ_NEW_SMALL_INT(1)));
475486

py/objint_longlong.c

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -247,45 +247,38 @@ mp_obj_t mp_obj_int_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs_i
247247
}
248248

249249
mp_obj_t mp_obj_new_int(mp_int_t value) {
250-
if (MP_SMALL_INT_FITS(value)) {
251-
return MP_OBJ_NEW_SMALL_INT(value);
252-
}
253250
return mp_obj_new_int_from_ll(value);
254251
}
255252

256253
mp_obj_t mp_obj_new_int_from_uint(mp_uint_t value) {
257-
// SMALL_INT accepts only signed numbers, so make sure the input
258-
// value fits completely in the small-int positive range.
259-
if ((value & ~MP_SMALL_INT_POSITIVE_MASK) == 0) {
260-
return MP_OBJ_NEW_SMALL_INT(value);
261-
}
262254
return mp_obj_new_int_from_ll(value);
263255
}
264256

265257
mp_obj_t mp_obj_new_int_from_ll(long long val) {
258+
if ((long long)(mp_int_t)val == val && MP_SMALL_INT_FITS(val)) {
259+
return MP_OBJ_NEW_SMALL_INT(val);
260+
}
261+
266262
mp_obj_int_t *o = mp_obj_malloc(mp_obj_int_t, &mp_type_int);
267263
o->val = val;
268-
return o;
264+
return MP_OBJ_FROM_PTR(o);
269265
}
270266

271267
mp_obj_t mp_obj_new_int_from_ull(unsigned long long val) {
272268
// TODO raise an exception if the unsigned long long won't fit
273269
if (val >> (sizeof(unsigned long long) * 8 - 1) != 0) {
274270
mp_raise_msg(&mp_type_OverflowError, MP_ERROR_TEXT("ulonglong too large"));
275271
}
276-
mp_obj_int_t *o = mp_obj_malloc(mp_obj_int_t, &mp_type_int);
277-
o->val = val;
278-
return o;
272+
return mp_obj_new_int_from_ll(val);
279273
}
280274

281275
mp_obj_t mp_obj_new_int_from_str_len(const char **str, size_t len, bool neg, unsigned int base) {
282276
// TODO this does not honor the given length of the string, but it all cases it should anyway be null terminated
283277
// TODO check overflow
284-
mp_obj_int_t *o = mp_obj_malloc(mp_obj_int_t, &mp_type_int);
285278
char *endptr;
286-
o->val = strtoll(*str, &endptr, base);
279+
mp_obj_t result = mp_obj_new_int_from_ll(strtoll(*str, &endptr, base));
287280
*str = endptr;
288-
return o;
281+
return result;
289282
}
290283

291284
mp_int_t mp_obj_int_get_truncated(mp_const_obj_t self_in) {

py/objint_mpz.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,14 @@ mp_obj_t mp_obj_int_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs_i
312312
return MP_OBJ_NULL; // op not supported
313313
}
314314

315+
// Check if the result fits in a small-int, and if so just return that.
316+
mp_int_t res_small;
317+
if (mpz_as_int_checked(&res->mpz, &res_small)) {
318+
if (MP_SMALL_INT_FITS(res_small)) {
319+
return MP_OBJ_NEW_SMALL_INT(res_small);
320+
}
321+
}
322+
315323
return MP_OBJ_FROM_PTR(res);
316324

317325
} else {
@@ -425,6 +433,10 @@ mp_int_t mp_obj_int_get_checked(mp_const_obj_t self_in) {
425433
const mp_obj_int_t *self = MP_OBJ_TO_PTR(self_in);
426434
mp_int_t value;
427435
if (mpz_as_int_checked(&self->mpz, &value)) {
436+
// mp_obj_int_t objects should always contain a value that is a large
437+
// integer (if the value fits in a small-int then it should have been
438+
// converted to a small-int object), and so this code-path should never
439+
// be taken in normal circumstances.
428440
return value;
429441
} else {
430442
// overflow

tests/basics/int_big_to_small.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
try:
2+
import micropython
3+
micropython.heap_lock
4+
except:
5+
print("SKIP")
6+
raise SystemExit
7+
8+
# All less than small int max.
9+
for d in (0, 27, 1<<29, -1861, -(1<<29)):
10+
i = 1<<70
11+
print(i)
12+
j = (1<<70) + d
13+
print(j)
14+
# k should now be a small int.
15+
k = j - i
16+
print(k)
17+
18+
# Now verify that working with k doesn't allocate (i.e. it's a small int).
19+
micropython.heap_lock()
20+
print(k + 20)
21+
print(k // 20)
22+
micropython.heap_unlock()

tests/basics/int_big_to_small.py.exp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
1180591620717411303424
2+
1180591620717411303424
3+
0
4+
20
5+
0
6+
1180591620717411303424
7+
1180591620717411303451
8+
27
9+
47
10+
1
11+
1180591620717411303424
12+
1180591620717948174336
13+
536870912
14+
536870932
15+
26843545
16+
1180591620717411303424
17+
1180591620717411301563
18+
-1861
19+
-1841
20+
-94
21+
1180591620717411303424
22+
1180591620716874432512
23+
-536870912
24+
-536870892
25+
-26843546

tests/ports/unix/extra_coverage.py.exp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,11 @@ data
8888
1
8989
12345
9090
6
91+
-1
9192
# runtime utils
9293
TypeError: unsupported type for __abs__: 'str'
9394
TypeError: unsupported types for __divmod__: 'str', 'str'
95+
0
9496
1
9597
2
9698
OverflowError: overflow converting long int to machine word

0 commit comments

Comments
 (0)