-
-
Notifications
You must be signed in to change notification settings - Fork 8.2k
py/objint.c: Add support for int.bit_length(). #11679
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
Code size report:
|
Codecov Report
@@ Coverage Diff @@
## master #11679 +/- ##
=======================================
Coverage 98.40% 98.40%
=======================================
Files 156 156
Lines 20609 20628 +19
=======================================
+ Hits 20281 20300 +19
Misses 328 328
|
Signed-off-by: Damiano Mazzella <damianomazzella@gmail.com>
Hi Wondering about the possible use-cases, and the impact on firmware size, Rationale:
With my proposal, I think we can have the two features, bit_length and bit_count, which require less memory than just the bit_length proposal above. IMHO these scaled down implementations will cover 99% of their real usage. Addendum: if for special cases (?) you need to use "big integers", you can code the two routines in python and use @micropython.native @dpgeorge, what do you think? |
A user cannot tell which internal representation is used for an integer, and Python code must just work irrespective of the internal representation of a number. With your proposal there is the risk that bit_length() and bit_count() sometimes work and sometimes not. That is hardly acceptable. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, this PR is +128 bytes on PYBV11.
I have implemented my suggestions in the review comments here: master...jimmo:micropython:int-bit-length
It comes out slightly smaller at +108 bytes.
I also tried a version that used builtin intrinsic bit operations (specifically CLZ). This didn't help with code size, and also because we still need to provide fallbacks it made the code a lot more complicated.
@@ -42,3 +42,8 @@ | |||
#define MICROPY_TRACKED_ALLOC (1) | |||
#define MICROPY_WARNINGS_CATEGORY (1) | |||
#define MICROPY_PY_UCRYPTOLIB_CTR (1) | |||
|
|||
// Enable int.bit_length(n) | |||
#ifndef MICROPY_INT_BIT_LENGTH |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to be defined (and defaulted) in mpconfig.h.
In general rather than enabling things in coverage, just make them MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EVERYTHING
in mpconfig, which will be enabled by coverage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also for consistency I think this should be called MICROPY_PY_BUILTINS_INT_BIT_LENGTH
.
return 0; | ||
} | ||
|
||
mpz_t *dest = mpz_clone(n); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of copying the mpz into a new integer and modifying it, you can just count the digits in the mpz.
#if MICROPY_INT_BIT_LENGTH | ||
STATIC mp_obj_t int_bit_length(size_t n_args, const mp_obj_t *args) { | ||
(void)n_args; | ||
#if MICROPY_LONGINT_IMPL == MICROPY_LONGINT_IMPL_MPZ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This unnecessarily creates an mpz if the input is a small int.
Instead, these functions should handle small ints separately, and only send them off to mpz if necessary.
#else | ||
mp_uint_t dest = MP_OBJ_SMALL_INT_VALUE(args[0]); | ||
mp_uint_t num_bits = 0; | ||
while (dest > 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does not work for negative numbers. It will always return 32 (or 64 depending on sizeof(mp_uint_t)
) as it will just keep shifting down the sign bit.
The reason the tests pass with negative numbers is that this code is never hit.
if (n == &n_temp) { | ||
mpz_deinit(n); | ||
} | ||
return mp_obj_new_int_from_ull(res); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
res will always fit in a small int, so can just use MP_OBJ_NEW_SMALL_INT directly.
#if MICROPY_INT_BIT_LENGTH | ||
mp_obj_t mp_obj_int_mpz_bit_length(mp_obj_t size) { | ||
mpz_t n_temp; | ||
mpz_t *n = mp_mpz_for_int(size, &n_temp); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As above, if you handle small ints directly, you don't need to allocate an mpz here.
print('SKIP') | ||
raise SystemExit | ||
|
||
n = -37 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needs tests for big ints (i.e. mpz). Also some more interesting bit patterns around the boundaries.
return mp_obj_new_int_from_uint(num_bits); | ||
#endif | ||
} | ||
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(int_bit_length_obj, 0, 1, int_bit_length); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We also need to provide this for the "long long" implementation.
As @robert-hh has pointed out, MicroPython's integer type is a "small integer" until it needs to be big. But the integer type itself is what says that it has a
There are operations like clz, but they are not universally available. Additionally, different compilers have different ways of accessing these instructions, plus we have to provide a fallback. Also they come with quirks (e.g. clz is undefined in the input is zero), which leads to more code size.
See my proposed version linked above, the MPZ version is actually simpler because MPZ works entirely in unsigned digits.
You still have to fork to decide at runtime whether the given integer is small or big. You end up with extra code to handle the big case anyway (e.g. raising an error).
Unfortunately this is not the case. These things have to be measured and it takes time and effort and the results can be counterintuitive. |
@jimmo good savings of bytes and optimizations, I flatly agree with your review. |
@jimmo wrote: I see, I was too focused on bit banging, where smallints are fine. If I need to, I'll write a specialized viper routine. |
A very nice code rewrite. Quite smart, it overtakes Damiano code and my attempt to improve it. Thanks to Damiano for the startup and to Jim for the improvement. |
Hi Jim Is it possible to add your bit_length to the milestones of MP 1.21 ? |
This is an automated heads-up that we've just merged a Pull Request See #13763 A search suggests this PR might apply the STATIC macro to some C code. If it Although this is an automated message, feel free to @-reply to me directly if |
Add support for int.bit_length()
Closes #4065 (feature request issue)