-
Notifications
You must be signed in to change notification settings - Fork 1
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
Add import-export API for Python int objects #35
Comments
To play with:
Later I'll show some simple benchmarks, but first please note that current code for gmpy2 do special cases for "small" integers to make performance acceptable. I think it's a common pattern and should be used in the second case as well (current code in the CPython main has a special case for 1-digit ints). That's why I would prefer more simple and less generic API, like proposed in #31. E.g. for reading: if (PyLong_IsCompact(obj)) { /* should be a quick check */
mpz_set_si(z, PyLong_CompactValue(obj)); /* compact value fits in some integer type */
}
else {
/* fallback to "array of digits" view for abs(obj) */
const Py_digit *digits = PyLong_GetDigits(obj); /* can't fail for non-compact ints */
mpz_import(z, ..., digits);
if (PyLong_IsNegative(obj) { /* looks better than PyLong_GetSign() ;) */
mpz_neg(z, z);
}
} (In this example, for simplicity, I assume that compact value fits in long.) |
Benchmarks (one, two, ~ten, ~hundred digits) for gmpy2.For exportOn the master:
With new API:
Without special case for 1-digit:
Using PyUnstable_Long_IsCompact (like in proposed above code):
New API overhead seems to be noticeable for 1-2 digits case (up to 10-15%). For importOn the master:
With new API:
|
If Otherwise, if we were to document this function as pairing with
Please, no more exported data. Make it a function that either returns a pointer to the layout, a function that copies the layout values into a struct, or integrate it with
Does this meant "most significant digit first"? Or "most significant byte of each digit first"? Or both? As we'd now use |
No, it's about endianness in the digit. We have Probably naming could be better. E.g.
That seems to be a good idea. |
I agree with @zooba, and I still stand by my earlier comment. I don't want to lock CPython -- and any future C API implementors -- into using a single layout for all ints. |
Do you mean adding a layout member to PyLong_DigitArray (export) and a layout parameter to typedef struct PyLong_DigitArray {
...
const PyLongLayout *layout; // <== NEW
} PyLong_DigitArray;
PyAPI_FUNC(int) PyLong_AsDigitArray(
PyObject *obj,
PyLong_DigitArray *array);
PyAPI_FUNC(PyLongWriter*) PyLongWriter_Create(
...
const PyLongLayout *layout); // <== NEW |
JFR, I don't think this locks CPython in any way: this single layout is an asymptotic one (for "big" ints). The API has enough freedom to emulate this view for small integers. Suggestion @zooba might work with API like (reading case): typedef struct PyLongLayout {
uint8_t bits_per_digit;
uint8_t digit_sizeof;
int8_t digits_order;
int8_t digit_endianness;
Py_ssize_t ndigits;
} PyLongLayout;
/* Returns a pointer to digit array (or NULL, if obj not PyLongObject) and
set layout. Which might be different wrt bits_per_digit and digit_sizeof for
"small" and "big" ints. */
PyAPI_FUNC(const void *) PyLong_AsDigits(PyObject *obj, PyLongLayout *layout); |
Currently, exporting a small integer as an array of digits is a O(1) operation. There is no need to allocate memory or anything, all integers are implemented internally as an array of digits. |
OK. Let's design API for what we have. If we need additional layouts, that kind of API can be added in the future, and if/when we add it, the API added here will start copying data. And if a non-CPython implementation uses a more elaborate scheme, it'll need to copy data too.
Same with the writer: We also need a We don't need endianness in the struct. I think something like a new medium-int representation (which would need a new API), is much more likely than switching to non-native endianness. As for avoiding
Would it work if you replace Looking at the above example: most callers will need the sign bit, so |
You meant word_endian too?
patch wrt gmpy2 test branchdiff --git a/src/gmpy2_convert_gmp.c b/src/gmpy2_convert_gmp.c
index 34f2546..e732080 100644
--- a/src/gmpy2_convert_gmp.c
+++ b/src/gmpy2_convert_gmp.c
@@ -44,24 +44,25 @@
static void
mpz_set_PyLong(mpz_t z, PyObject *obj)
{
- static PyLong_DigitArray long_export;
-
- PyLong_AsDigitArray(obj, &long_export);
- if (long_export.ndigits == 1) {
- mpz_set_si(z, long_export.digits[0]);
+ long value;
+ Py_ssize_t bytes = PyLong_AsNativeBytes(obj, &value, sizeof(long), -1);
+ if (0 <= bytes && bytes <= (Py_ssize_t)sizeof(long)) {
+ mpz_set_si(z, value);
}
else {
+ static PyLong_DigitArray long_export;
+ PyLong_AsDigitArray(obj, &long_export);
mpz_import(z, long_export.ndigits,
PyLong_LAYOUT.array_endian,
PyLong_LAYOUT.digit_size,
PyLong_LAYOUT.word_endian,
PyLong_LAYOUT.digit_size*8 - PyLong_LAYOUT.bits_per_digit,
long_export.digits);
+ if (long_export.negative) {
+ mpz_neg(z, z);
+ }
+ PyLong_FreeDigitArray(&long_export);
}
- if (long_export.negative) {
- mpz_neg(z, z);
- }
- PyLong_FreeDigitArray(&long_export);
}
static MPZ_Object *
Not necessary. This view rather represents the absolute value. |
Yes. I see no reason to single that out as a detail that might change.
Hm, I guess that's not good enough?
I meant that instead of
you'd do
|
Yes, especially the second case. You can see other benchmarks above.
Yes, if PyLong_AsNativeBytes can do a quick exit - that should be an alternative to PyLong_IsCompact/CompactValue.
This looks as a minor convenience for me. I think to query (optionally) sign people should use dedicated functions (like PyLong_IsNegative()). |
Oh, I forgot we already have If you're chasing nanoseconds, I don't think you want to compile for stable ABI, and that you will want to change your code if the internals change. So, (That said, consider having the optimizations in |
I renamed
Ok, I modified the API to add
|
The function |
Yes, but see #29 |
If I'm reading the earlier benchmarks correctly, then we'd have to beat 111ns and 155ns for these, which is clearly only going to be possible by leaking access to Long internals. So it's fine for when using unstable APIs, and the slower path is probably fine for stable callers (it's still faster than failing ;) ). The difference is largely unavoidable function call overhead, a type check, and the (potentially unaligned) memcpy. I daresay we could add a faster path through But that's okay, blazing speed is what the unstable API is for, and those who need it can at least opt into using it at their own cost. I do think that anything returning somewhat raw digits from a LongObject should also return the sign. One call to get all the info to accurately recreate the value, and you only need additional checks if you're creating your own fast path. |
Ok, done.
I would prefer to avoid adding such function for now, we can add it later if needed. What do you think? |
@skirpichev @zooba @encukou: I updated the API. What do you think? Are we good yet? |
Is there any impact going to come out of the discussions of setting a hard (if theoretical) limit on the length? |
@serhiy-storchaka may know about that :-) This API is limited by The PR python/cpython#123498 defines |
So, to destroy the writer (e.g. in an error handling path), the user should call That works, but I'd much prefer writers to consistently and have both |
Sorry for a late answer @zooba, but no. Those timings are for writing API (mpz->int). Above benchmarks are for reading (int->mpz). I'll repeat them in one place: For exportOn the master:
With new API (see aleaxit/gmpy#495):
Without special case for 1-digit:
Using PyUnstable_Long_IsCompact (like in proposed above code):
"if you replace PyLong_IsCompact and PyLong_CompactValue with PyLong_AsNativeBytes and a digit_size buffer":
So, using a quick check (currently with PyUnstable_Long_IsCompact) to fail on special functions for small integers, else use PyLong_GetDigits() (only one new function for reading digits) - has better performance then current API. Maybe you are right and we should introduce a bunch of simple PyUnstable_* functions instead of this more complicated API, which has performance regressions.
I doubt that using single layout for "big enough" integers leaks something important. All MP libraries share that detail, hardly CPython will break this in a future.
I like changes. |
I'm wrong. MAX_LONG_DIGITS fits into signed |
You can prepare your data in advance before creating a writer, so later, your code cannot fail.
Ok, I added a |
I open a vote on this API: |
I would appreciate @casevh opinion, but so far this API has severe performance degradation for reading small integers (int->mpz conversion), up to 25%. Hardly it's acceptable. So, probably projects like gmpy2, sage or flint-python - will use some other code path for such integers.
Yet maybe we should use size_t here, like GMP? Signed integer type looks odd in this context. |
We want to preserve code that has already been compiled, which means if we change the size of the digits but we already leaked that into other compiled code (i.e. hardcoded from a macro), then we've broken it. The whole point of the The stable digit size is 8 bits, and personally I'm quite okay with the performance hit for that stability. But if we are also to guarantee a second digit size with the same stability then we either risk adding a performance hit later (if we now have to start converting digits) or get stuck never gaining anything because we aren't able to change the internal representation. So the best you're going to get here is "fast+unstable (recompile each release)" or "fast+dynamic digit size".
Only thing I want to change here is the |
My apologies for not following this thread as much as I would have liked. I'll also defer to @skirpichev for the "is it satisfactory" question. I started optimizing gmpy around version 1.10. The single biggest win was using mpz_import/mpz_export directly into the PyLong internals. The only change to the code was support for 30-bit digits. The same code worked from Python 2.5 through Python 3.11. But this did require version specific builds. The second win was using the PyLong_AsLongAndOverflow (and its variants) functions for conversion of Python ints into temporary C ints for direct use in many of the GMP function calls. Optimizing the performance of (mpz * 3) + 1 was more important than optimizing mpz(3) and mpz(1) because most of my code did mixed arithmetic. Note: the performance win was primarily from avoiding the creation of an exception. As long as access to an unstable interface remains, I accept that the stable interface may be slightly slower. If the difference is significant, we will need to release stable and unstable versions. Thanks for all the effort. |
Ah, PyLong_GetDigits() declaration has
+1 I'm also not sure if we should keep layout field in the PyLong_DigitArray struct. What else it could be if not native layout?! BTW, all required information about native layout is already available via PyLong_GetInfo(). PyLongLayout has additionally
@casevh, here are some benchmarks. Does this look acceptable for you? |
Have you tried PyLong_AsLongAndOverflow instead of PyUnstable_Long_IsCompact? It might not be as fast but it is stable (I think). |
Yes, I did, just not included this case in above benchmarks. It's good for 1-2 digits and for many digits, but for ~10 - performance has negative impact. E.g. in the master:
With proposed API & PyLong_AsLongAndOverflow:
Hmm, maybe it's a good tradeoff. |
Yes, later we can add |
Ok, I updated the API:
|
@skirpichev: IMO it's fine to rely on |
AFAIK, Python uses |
@skirpichev: are you ok to use signed Py_ssize_t? |
This still look odd for me, but I agree with Petr - Py_ssize_t seems to be more consistent with the CPython API. size_t used in *alloc() functions. |
Ok, I updated the API again to move back to signed |
API designed with the help of @skirpichev who works on gmpy2.
@skirpichev ran benchmarks on gmpy2 and it seems like the performance overhead is acceptable. The performance is almost the same compared to the code accessing directly to Python int (PyLongObject) internals.
PyLongLayout
Data needed by GMP import-export functions.
Export: PyLong_AsDigitArray()
Export a Python int as an array of digits.
On CPython, no memory copy is needed, it's just a thin wrapper to expose Python int internal array of digits. The advantage is that
PyLong_DigitArray.obj
stores a strong reference to the Python int object, to make sure that that structure remains valid untilPyLong_FreeDigitArray()
is called.Import: PyLongWriter_Create()
Create a Python int object from an array of digits.
It's a thin wrapper to the private
_PyLong_New()
function.PyLongWriter_Finish()
takes care of normalizing the digit and convert the object to a compact long if needed.Discussions
See also:
The text was updated successfully, but these errors were encountered: