Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

gh-102471, PEP 757: Add PyLong import and export API#121339

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
vstinner merged 54 commits intopython:mainfromvstinner:long_export
Dec 13, 2024

Conversation

@vstinner
Copy link
Member

@vstinnervstinner commentedJul 3, 2024
edited by github-actionsbot
Loading

Add PyLong_Export() and PyLong_Import() functions and PyLong_LAYOUT structure.


📚 Documentation preview 📚:https://cpython-previews--121339.org.readthedocs.build/

@vstinner
Copy link
MemberAuthor

vstinner commentedJul 3, 2024
edited
Loading

cc@skirpichev@casevh

@vstinner
Copy link
MemberAuthor

See also issue#111415

Copy link
Contributor

@skirpichevskirpichev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Just my 2c.

The gmpy2 code, used for benchmarking, can be found in my fork:
https://github.com/skirpichev/gmpy/tree/trying-py-import-export

Comment on lines 6705 to 6763
PyUnstable_Long_Export(PyObject*obj,PyUnstable_LongExport*long_export)
{
if (!PyLong_Check(obj)) {
PyErr_Format(PyExc_TypeError,"expect int, got %T",obj);
return-1;
}
PyLongObject*self= (PyLongObject*)obj;

long_export->obj= (PyLongObject*)Py_NewRef(obj);
long_export->negative=_PyLong_IsNegative(self);
long_export->ndigits=_PyLong_DigitCount(self);
if (long_export->ndigits==0) {
long_export->ndigits=1;
}
long_export->digits=self->long_value.ob_digit;
return0;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

As this mostly give a direct access to the PyLongObject - it almost as fast as using private stuff before.

Old code:

$ python -m timeit -r20 -s 'from gmpy2 import mpz;x=10**2' 'mpz(x)'1000000 loops, best of 20: 232 nsec per loop$ python -m timeit -r11 -s 'from gmpy2 import mpz;x=10**100' 'mpz(x)'500000 loops, best of 11: 500 nsec per loop$ python -m timeit -r20 -s 'from gmpy2 import mpz;x=10**1000' 'mpz(x)'100000 loops, best of 20: 2.53 usec per loop

With proposed API:

$ python -m timeit -r20 -s 'from gmpy2 import mpz;x=10**2' 'mpz(x)'1000000 loops, best of 20: 258 nsec per loop$ python -m timeit -r20 -s 'from gmpy2 import mpz;x=10**100' 'mpz(x)'500000 loops, best of 20: 528 nsec per loop$ python -m timeit -r20 -s 'from gmpy2 import mpz;x=10**1000' 'mpz(x)'100000 loops, best of 20: 2.56 usec per loop

vstinner reacted with thumbs up emoji
Comment on lines 6697 to 6742
PyObject*
PyUnstable_Long_Import(intnegative,size_tndigits,Py_digit*digits)
{
return (PyObject*)_PyLong_FromDigits(negative,ndigits,digits);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

But this is something I would like to avoid. This requires allocation of a temporary buffer and using memcpy. Can we offer awritable layout to use it's digits in the mpz_export directly?

Benchmarks for old code:

$ python -m timeit -r11 -s 'from gmpy2 import mpz;x=mpz(10**2)' 'int(x)'2000000 loops, best of 11: 111 nsec per loop$ python -m timeit -r11 -s 'from gmpy2 import mpz;x=mpz(10**100)' 'int(x)'500000 loops, best of 11: 475 nsec per loop$ python -m timeit -r11 -s 'from gmpy2 import mpz;x=mpz(10**1000)' 'int(x)'100000 loops, best of 11: 2.39 usec per loop

With new API:

$ python -m timeit -r20 -s 'from gmpy2 import mpz;x=mpz(10**2)' 'int(x)'2000000 loops, best of 20: 111 nsec per loop$ python -m timeit -r20 -s 'from gmpy2 import mpz;x=mpz(10**100)' 'int(x)'500000 loops, best of 20: 578 nsec per loop$ python -m timeit -r20 -s 'from gmpy2 import mpz;x=mpz(10**1000)' 'int(x)'100000 loops, best of 20: 2.53 usec per loop

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

This requires allocation of a temporary buffer and using memcpy.

Right, PyLongObject has to manage its own memory.

Can we offer a writable layout to use it's digits in the mpz_export directly?

That sounds strange from the Python point of view and make the internals "less opaque". I would prefer to leak less implementation details.

Copy link
Contributor

@skirpichevskirpichevJul 4, 2024
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Right, PyLongObject has to manage its own memory.

I'm not trying to change that. More complete proposal:vstinner#4

gmpy2 patch:https://github.com/skirpichev/gmpy/tree/trying-py-import-export-v2

New benchmarks:

$ python -m timeit -r20 -s 'from gmpy2 import mpz;x=mpz(10**2)' 'int(x)'2000000 loops, best of 20: 111 nsec per loop$ python -m timeit -r20 -s 'from gmpy2 import mpz;x=mpz(10**100)' 'int(x)'500000 loops, best of 20: 509 nsec per loop$ python -m timeit -r20 -s 'from gmpy2 import mpz;x=mpz(10**1000)' 'int(x)'100000 loops, best of 20: 2.44 usec per loop

I would prefer to leak less implementation details.

I don't think this leak anything. It doesn't leak memory management details.PyLong_Import will just do allocation memory. Writting digits will be job for mpz_export, as before.

Without this, it seems - there are noticeable performance regression for integers of intermediate range. Something up to 20% vs 7% on my branch.

Edit: currently, proposedPyUnstable_Long_ReleaseImport() matchPyUnstable_Long_ReleaseExport(). Perhaps, it could be one function (say, PyUnstable_Long_ReleaseDigitArray()), but I unsure - maybe it puts some constraints on internals of the PyLongObject.

@skirpichev
Copy link
Contributor

CC@tornaria, as Sage people might be interested in this feature.

@skirpichev
Copy link
Contributor

CC@oscarbenjamin, you may want this for python-flint

@vstinner
Copy link
MemberAuthor

I updated my PR:

  • Use -1 for little endian and +1 for big endian.
  • Rename PyUnstable_LongExport to PyUnstable_Long_DigitArray.
  • Add "always succeed" mention in the doc.
skirpichev reacted with thumbs up emoji

@oscarbenjamin
Copy link
Contributor

CC@oscarbenjamin, you may want this for python-flint

Absolutely. Currently python-flint uses a hex-string as an intermediate format when converting between large int andfmpz so anything more direct is an improvement. I expect that python-flint would usempz_import/mpz_export just like gmpy2.

@vstinner
Copy link
MemberAuthor

@skirpichev: I added a PyLongWriter API similar to what@encukou proposed.

Example:

PyLongObject*_PyLong_FromDigits(intnegative,Py_ssize_tdigit_count,digit*digits){PyLongWriter*writer=PyLongWriter_Create();if (writer==NULL) {returnNULL;    }if (negative) {PyLongWriter_SetSign(writer,-1);    }Py_digit*writer_digits=PyLongWriter_AllocDigits(writer,digit_count);if (writer_digits==NULL) {        gotoerror;    }memcpy(writer_digits,digits,digit_count*sizeof(digit));return (PyLongObject*)PyLongWriter_Finish(writer);error:PyLongWriter_Discard(writer);returnNULL;}

ThePyLongWriter_Finish() function normalizes the number and gets a small number if needed. Example:

>>>import_testcapi;_testcapi.pylong_import(0, [100,0,0])is100True

@vstinnervstinner marked this pull request as draftJuly 4, 2024 13:00
@vstinner
Copy link
MemberAuthor

I mark the PR as a draft until we agree on the API.

@skirpichev
Copy link
Contributor

I added a PyLongWriter API similar to what@encukou proposed.

Yes, that looks better and should fix speed regression. I'll try to benchmark that, perhaps tomorrow.

But cost is 5 (!) public functions and one new struct, additionally toPyUnstable_Long_Import(), which will be a more slow API. Correct? C.f. just one function inabove proposal.

@vstinner
Copy link
MemberAuthor

I updated the PR to remove thePyUnstable_ prefix, replace it with thePyLong prefix.

@vstinner
Copy link
MemberAuthor

But cost is 5 (!) public functions and one new struct, additionally to PyUnstable_Long_Import(), which will be a more slow API. Correct? C.f. just one function in#121339 (comment) proposal.

My concern is to avoid the problemcapi-workgroup/api-evolution#36 : avoid exposing_PyLong_New() object until it's fully initialized. The "writer" API hides the implementation details but also makes sure that the object is not "leaked" to Python before it's fully initialized and valid. By the way, the implementation uses functions which are only safe if the object cannot be seen in Python: if Py_REFCNT(obj) is 1.

zooba reacted with thumbs up emoji

@vstinner
Copy link
MemberAuthor

@skirpichev: Would it be useful to add aPyLongWriter_SetValue(PyLongWriter *writer, long value) function? It would be similar toPyLong_FromLong(long value) (but may be less efficient), so I'm not sure if it's relevant.

Copy link
Member

@picnixzpicnixz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

For users, I think it'd be great if they know (in the docs) whether something can be NULL or not, especially since we have a similar interface for unicode objects.

Comment on lines +234 to +236
digit*digits=PyMem_Malloc((size_t)ndigits*sizeof(digit));
if (digits==NULL) {
returnPyErr_NoMemory();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

The docs say that we should check for NULL but agreed that we could update them.

Comment on lines +234 to +236
digit*digits=PyMem_Malloc((size_t)ndigits*sizeof(digit));
if (digits==NULL) {
returnPyErr_NoMemory();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Maybe bug is somewhere else? Newly added functions usually are explicitly say if they set an exception on error.

Comment on lines +6805 to +6812
intoverflow;
#ifSIZEOF_LONG==8
longvalue=PyLong_AsLongAndOverflow(obj,&overflow);
#else
// Windows has 32-bit long, so use 64-bit long long instead
long longvalue=PyLong_AsLongLongAndOverflow(obj,&overflow);
#endif
Py_BUILD_ASSERT(sizeof(value)==sizeof(int64_t));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Another reason for using this API was "no-error" contract. Maybe we can specify that in docs as a CPython implementation detail? IIUC, we are free to change such things in new releases without deprecation period.

Note also, that gmpy2 benchmarks measure not just CPython side, but the whole conversion path (int->gmpy2.mpz in this case).

If *export_long->digits* is not ``NULL``,:c:func:`PyLong_FreeExport` must
be called when the export is no longer needed.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Suggested change
..impl-detail::
This function always succeeds if *obj* is a Python:class:`int` object
or a subclass.

Lets see if we can restore this in a that way. It might be helpful for e.g. Sage, which doesn't support PyPy.

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I would prefer to not add this note. It was controversial during PEP 757 design.

@serhiy-storchaka@encukou: What do you think? Would you be ok to declare that the PyLong_Export() function cannot fail if the argument is a Python int?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

It was controversial during PEP 757 design.

It was proposed unconditionally, not as CPython's implementation detail.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I'm fine with it, as an implementation detail.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

@vstinner
Copy link
MemberAuthor

@picnixz: I addressed your review. Please review the updated PR.

Copy link
Member

@picnixzpicnixz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Final nits and LGTM. Since I'm on mobile I don't know whether the spaces are correct or not so please check it manually.

vstinner and skirpichev reacted with thumbs up emoji
Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
@vstinner
Copy link
MemberAuthor

Thanks for all reviews. I think that the change is now ready to be merged, I addressed all comments. I plan to merge the PR Friday.

@skirpichev
Copy link
Contributor

I plan to merge the PR Friday.

I would appreciate this, but... Isn't now an approval from other core developer is required for this type of pr's?

Copy link
Member

@serhiy-storchakaserhiy-storchaka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Update alsoDoc/data/refcounts.dat.

Comment on lines +6805 to +6812
intoverflow;
#ifSIZEOF_LONG==8
longvalue=PyLong_AsLongAndOverflow(obj,&overflow);
#else
// Windows has 32-bit long, so use 64-bit long long instead
long longvalue=PyLong_AsLongLongAndOverflow(obj,&overflow);
#endif
Py_BUILD_ASSERT(sizeof(value)==sizeof(int64_t));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

This code is so close to the PyLong implementation, that I think we should use_PyLong_IsCompact() +_PyLong_CompactValue() to be sure that it matches the specification.

Comment on lines +6828 to +6830
if (export_long->ndigits==0) {
export_long->ndigits=1;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Why?

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

It's to make the API easier to use: the consumer doesn't have to both with ndigits==0 special case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

In fact it's assumed (see e.g._PyLong_New), that the digit array always has at least one digit (and it's initialized for 0 too). And also, if you pass ndigits==0 to the mpz_import() ascount parameter, then it just calls malloc(0).

But I think it's safe just drop this check. This condition is unreachible here, as0 handled in the!overflow case.

Suggested change
if (export_long->ndigits==0) {
export_long->ndigits=1;
}

PyLongWriter*
PyLongWriter_Create(intnegative,Py_ssize_tndigits,void**digits)
{
if (ndigits <=0) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Why not allow 0?

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Actually, he asked to document that case (i.e. when ndigits==0).

We don't think it's a good idea to overbloat docs with this edge case (different functions should be used for small integers). PEP has a dedicated section to discuss import for small integers, suggesting different functions. (In fact, the whole API is about import/export for big integers.)

Copy link
Member

@encukouencukou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Looks good, though I have a few suggestions.

Also, please test thatPyLong_FreeExportcan be used afterPyLong_Export fills in the compactvalue. AFAICS, the tests now skip it if they can.

If *export_long->digits* is not ``NULL``,:c:func:`PyLong_FreeExport` must
be called when the export is no longer needed.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I'm fine with it, as an implementation detail.

@vstinner
Copy link
MemberAuthor

@serhiy-storchaka and@encukou: I addressed your reviews. Please review the updated PR.

@serhiy-storchaka:

Update also Doc/data/refcounts.dat.

I addedPyLong_Export() andPyLongWriter_Finish() to Doc/data/refcounts.dat, the only function which takes aPyObject*.

@encukou:

Also, please test that PyLong_FreeExport can be used after PyLong_Export fills in the compact value. AFAICS, the tests now skip it if they can.

Done.

Copy link
Member

@serhiy-storchakaserhiy-storchaka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

LGTM.

Please do not forget to edit the commit message before merging.

@vstinnervstinner merged commit6446408 intopython:mainDec 13, 2024
45 checks passed
@vstinner
Copy link
MemberAuthor

Ok, I merged the PR. Big thanks to everyone who reviewed this PR which got 272 comments and 54 commits! Special thanks to@skirpichev who wrote a big part of this work.

encukou, erlend-aasland, and oscarbenjamin reacted with hooray emoji

@skirpichev
Copy link
Contributor

IIUIC, Now anonymous unionsare allowed.

Maybe we could use them in the export API in this way:

typedefstructPyLongExport2 {union {int64_tcompact_value;struct {uint8_tnegative;Py_ssize_tndigits;constvoid*digits;Py_uintptr_t_reserved;        }digit_array;    };}PyLongExport2;

?

This structure has smaller size. Also, such API make a more clear distinction for alternative views.

@vstinner ?

@vstinner
Copy link
MemberAuthor

Also, such API make a more clear distinction for alternative views.

How do you know if compact_value or digit_array must be used?

@skirpichev
Copy link
Contributor

How do you know if compact_value or digit_array must be used?

By PyLong_Export's return value. Errors < 0. Nonnegative values - for possible export types.

@vstinner
Copy link
MemberAuthor

IIUIC, Now anonymous unionscapi-workgroup/decisions#30 (comment). Maybe we could use them in the export API in this way: (...)

PEP 757 had to go through the C API Working Group and then the Steering Council. I don't think that this change is worth it to restart this validation process.

Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment

Reviewers

@encukouencukouencukou left review comments

@ericsnowcurrentlyericsnowcurrentlyericsnowcurrently left review comments

@zoobazoobazooba left review comments

@skirpichevskirpichevskirpichev approved these changes

@erlend-aaslanderlend-aaslanderlend-aasland left review comments

@serhiy-storchakaserhiy-storchakaserhiy-storchaka approved these changes

@picnixzpicnixzpicnixz approved these changes

@corona10corona10Awaiting requested review from corona10

@pitroupitrouAwaiting requested review from pitrou

+1 more reviewer

@steve-ssteve-ssteve-s left review comments

Reviewers whose approvals may not affect merge requirements

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

11 participants

@vstinner@skirpichev@oscarbenjamin@zooba@encukou@picnixz@ericsnowcurrently@steve-s@pitrou@serhiy-storchaka@erlend-aasland

[8]ページ先頭

©2009-2025 Movatter.jp