Skip to content
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

gh-102471: convert decimal module to use PyLong_Export API (PEP 757) #128267

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

skirpichev
Copy link
Member

@skirpichev skirpichev commented Dec 26, 2024

For reviewers: in the history included version, where PyLong_Export always set digits.

Benchmark ref patch no-value
Decimal(1<<7) 735 ns not significant 721 ns: 1.02x faster
Decimal(1<<38) 809 ns 738 ns: 1.10x faster 846 ns: 1.05x slower
Decimal(1<<300) 1.93 us 2.05 us: 1.06x slower 1.99 us: 1.03x slower
Geometric mean (ref) 1.01x faster 1.01x slower

Benchmark hidden because not significant (1): Decimal(1<<3000)

benchmark code
# export_bench.py
import pyperf
from decimal import Decimal as D

runner = pyperf.Runner()
i1, i2, i3, i4 = 1 << 7, 1 << 38, 1 << 300, 1 << 3000
runner.bench_func('Decimal(1<<7)', D, i1)
runner.bench_func('Decimal(1<<38)', D, i2)
runner.bench_func('Decimal(1<<300)', D, i3)
runner.bench_func('Decimal(1<<3000)', D, i4)

Comment on lines +2339 to +2341
const uint32_t base = (uint32_t)1 << layout->bits_per_digit;
const uint8_t sign = export_long.negative ? MPD_NEG : MPD_POS;
const Py_ssize_t len = export_long.ndigits;
Copy link
Member

Choose a reason for hiding this comment

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

Please remove redundant consts.

Copy link
Member

Choose a reason for hiding this comment

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

Should not we check if layout->bits_per_digit >= 32?

Copy link
Member Author

Choose a reason for hiding this comment

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

Please remove redundant consts.

Which one looks redundant for you?

Should not we check if layout->bits_per_digit >= 32?

I was thinking about an assert. Do you think it's ok?

Copy link
Member

Choose a reason for hiding this comment

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

Which one looks redundant for you?

All consts applied to POD types are redundant. 3 lines here and 1 line for export_long.value.

Some style guides require to add const for all variables and function parameters, but this in not in CPython style guide. It makes the code too verbose and more difficult to read.

I was thinking about an assert. Do you think it's ok?

It is okay for now. But if we want to make it an exemplary code, we should remove dependence on CPython implementation details.

Copy link
Member Author

Choose a reason for hiding this comment

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

All consts applied to POD types are redundant.

Why? Doing something like value += 1 currently will produce an error at compilation time, while without const - not.

It makes the code too verbose and more difficult to read.

It emphasize programmer intentions not just for compiler, but for human being as well.

It is okay for now.

Ok, then I'll add assertions for all assumptions, that are silent not (endianness, digit order, etc). I think it's fine for a stdlib module. Caching in the module state seems overkill for this.

#else
#error "PYLONG_BITS_IN_DIGIT should be 15 or 30"
#endif
if (base > UINT16_MAX) {
Copy link
Member

Choose a reason for hiding this comment

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

We should use digit_size to chose between mpd_qimport_u32 and mpd_qimport_u16. Values other than 2 or 4 are errors.

Copy link
Member Author

Choose a reason for hiding this comment

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

You are assuming that CHAR_BIT == 8. I'm not sure we should do this.

With added assert(layout->bits_per_digit <= 32) above (or RuntimeError) - I think nothing should be changed here.

Copy link
Member

Choose a reason for hiding this comment

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

We are assuming that CHAR_BIT == 8 everywhere in the code.

The new C API is used to untie the decimal code from the CPython implementation details. Other implementations can have bits_per_digit == 8 and digit_size == 1 or bits_per_digit == 16 and digit_size == 2. While it is not particularly reasonable, but it is even possible to have bits_per_digit < 16 and digit_size > 2. It is better to fail with a relevant error message than produce incorrect result or crash.

Doing the runtime check for every conversion can be too costly, so we can do this only at the module import (and cache the base value).

We should also check digits_order and digit_endianness. mpd_qimport_u16 and mpd_qimport_u32 only work if digits_order is -1 and digit_endianness is native. In other cases we should create a temporary digit array with expected order and endianness. Or at least raise a runtime error.

if (-(int64_t)UINT32_MAX <= value && value <= (int64_t)UINT32_MAX) {
_dec_settriple(dec, value < 0 ? MPD_NEG : MPD_POS,
(uint32_t)Py_ABS(value), 0);
mpd_qfinalize(MPD(dec), ctx, status);
Copy link
Member

Choose a reason for hiding this comment

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

Is it worth it to have this fast-path? (Why not always calling mpd_qset_i64()?)

Copy link
Member Author

Choose a reason for hiding this comment

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

It's up to reviewers. In old pr I did benchmarks:
#127925 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants