Skip to content

Conversation

@markshannon
Copy link
Member

@markshannonmarkshannon commented Mar 6, 2023

This PR rearranges the bits in what was ob_size, to slightly speedup the most common operations and to prepare for storing the tagged 2-complement value directly in a future PR.

The new layout is as follows:

  • Bits 0 and 1: 1 - sign. I.e. 0 for positive numbers, 1 for zero and 2 for negative numbers.
  • Bit 2 reserved (probably for the immortal bit)
  • Bits 3+ the unsigned size.

The bulk of the change is removing all the uses of Py_SIZE and Py_SETSIZE, and replacing them with a new set of inline functions.
It disturbs me how much we use unchecked casts, but that's a separate issue...

This will, inevitably, break Cython generated code again.

Performance measurement shows no significant change: https://github.com/faster-cpython/benchmarking/tree/main/results/bm-20230302-3.12.0a5%2B-ce6bfb2

…Long_SignedDigitCount which might not be optimal, but is safe.
Copy link
Member

@gvanrossumgvanrossum left a comment

Choose a reason for hiding this comment

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

As my flight might depart soon here's a first batch of review comments. Still to do longobject.c, and some modules.

Comment on lines 97 to 98
ifnot (0<=n<=self.end-self.pos):
print(n, self.end, self.pos)
Copy link
Member

Choose a reason for hiding this comment

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

Remove debug print()?

Suggested change
ifnot (0<=n<=self.end-self.pos):
print(n, self.end, self.pos)

@@ -0,0 +1,7 @@
Rearrage bits in first field (after header) of PyLongObject. * Bits 0 and 1:
1- sign. I.e. 0 for positive numbers, 1 for zero and 2 for negative numbers.
Copy link
Member

Choose a reason for hiding this comment

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

Use consistent spacing around binary -.

Suggested change
1- sign. I.e. 0 for positive numbers, 1 for zero and 2 for negative numbers.
1- sign. I.e. 0 for positive numbers, 1 for zero and 2 for negative numbers.

return (a->long_value.lv_tag | b->long_value.lv_tag) < (2 << NON_SIZE_BITS);
}

/* The value returned by this function will have at least one bit to spare,
Copy link
Member

Choose a reason for hiding this comment

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

"one bit to spare" feels ambiguous, since the return type is signed -- is the spare bit the sign bit, or should there be at least one additional spare bit? (I know in practice we have 4 spare bits including the sign, but still, I'm not sure whether a 63-bit digit would be acceptable or not, from this description (or others).)

staticinlinebool
_PyLong_IsPositive(constPyLongObject*op)
{
return (op->long_value.lv_tag&SIGN_MASK) ==0;
Copy link
Member

Choose a reason for hiding this comment

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

Why not have #define SIGN_POSITIVE 0?

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

I want these functions to be the only way to determine the sign.
Defining SIGN_POSITIVE will just encourage people to do the test elsewhere.

Copy link
Member

Choose a reason for hiding this comment

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

Sure, fine. Next question: maybe we also need a _PyLong_IsNonZero? I see !_PyLong_IsZero a lot, and the ! is easily missed. (Or maybe that's just my old eyes.) Possibly also IsNonNegative and IsNonPositive.

returnop->long_value.lv_tag >> NON_SIZE_BITS;
}

/* Equivalent to _PyLong_DigitCount(op) * _PyLong_NonZeroSign(op) */
Copy link
Member

Choose a reason for hiding this comment

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

I take it this is for algorithms where the old "signed size" representation worked well?

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

It is for code that uses the "signed size" representation.
I make no judgement as to how well it works 🙂

return (a->long_value.lv_tag&SIGN_MASK) == (b->long_value.lv_tag&SIGN_MASK);
}

#defineTAG_FROM_SIGN_AND_SIZE(sign, size) ((1 - (sign)) | ((size) << NON_SIZE_BITS))
Copy link
Member

Choose a reason for hiding this comment

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

So maybe add a comment that this macro should only be used with literal or size_t arguments?

#defineTAG_FROM_SIGN_AND_SIZE(sign, size) ((1 - (sign)) | ((size) << NON_SIZE_BITS))

staticinlinevoid
_PyLong_SetSignAndSize(PyLongObject*op, intsign, Py_ssize_tsize)
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this use DigitCount instead of Size, for consistency with earlier APIs? Same for the next one.

staticinlinevoid
_PyLong_FlipSign(PyLongObject*op){
unsigned intflipped_sign=2- (op->long_value.lv_tag&SIGN_MASK);
op->long_value.lv_tag &= ~7;
Copy link
Member

Choose a reason for hiding this comment

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

I think you want to use some defined name instead of hardcoding 7? Perhaps ~((1 << NON_SIZE_BITS) - 1).

Comment on lines 2504 to 2506
returnPyLong_FromLong(i_result);
}
if (PyLong_CheckExact(item) ||PyBool_Check(item)){
Copy link
Member

Choose a reason for hiding this comment

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

The GitHub warning is at the wrong line, it applies to the PyLong_FromLong(i_result) two lines up. It does seem to warrant some attention. Similar below.

{
if (PyLong_Check(v) &&PyLong_Check(w) &&Py_SIZE(v) &&Py_SIZE(w)){
if (PyLong_Check(v) &&PyLong_Check(w) &&
!_PyLong_IsZero((PyLongObject*)v) && !_PyLong_IsZero((PyLongObject*)w)
Copy link
Member

Choose a reason for hiding this comment

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

Maybe we need another convenience macro IsNonZero.

Comment on lines +205 to +212
staticinlineint
_PyLong_CompactSign(constPyLongObject*op)
{
assert(PyLong_Check(op));
assert(_PyLong_IsCompact(op));
return1- (op->long_value.lv_tag&SIGN_MASK);
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this be the new implementation of _PyLong_Sign(), if _PyLong_NonCompactSign() is removed? This gets rid of a branch in the proposed version of _PyLong_Sign().

Copy link
Member

Choose a reason for hiding this comment

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

They sure look identical to me. Maybe Mark has plans and maybe the compiler would optimize this anyway?

if (P(x)) return F(x); else return F(x); 

could just become return F(x);.

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

We want the freedom to implement the "compact" and non-compact forms differently.
They have the same implementation at the moment, but that will change.

_PyLong_Sign() is part of the ABI, so we need to retain it. But almost all code using _PyLong_Sign() actually wants to know if an int is negative and should be using _PyLong_IsNegative().

Copy link
Member

@gvanrossumgvanrossum left a comment

Choose a reason for hiding this comment

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

Here's the rest. I went over every diff chunk in longobject.c. Let's get this merged...

Comment on lines 1 to 4
Rearrage bits in first field (after header) of PyLongObject. * Bits 0 and 1:
1 - sign. I.e. 0 for positive numbers, 1 for zero and 2 for negative numbers.
* Bit 2 reserved (probably for the immortal bit) * Bits 3+ the unsigned
size.
Copy link
Member

Choose a reason for hiding this comment

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

Format as bullets?

Suggested change
Rearrage bits in first field (after header) of PyLongObject. * Bits 0 and 1:
1 - sign. I.e. 0 for positive numbers, 1 for zero and 2 for negative numbers.
* Bit 2 reserved (probably for the immortal bit) * Bits 3+ the unsigned
size.
Rearrage bits in first field (after header) of PyLongObject:
* Bits 0 and 1: 1 - sign. I.e. 0 for positive numbers, 1 for zero and 2 for negative numbers.
* Bit 2 reserved (probably for the immortal bit).
* Bits 3+ the unsigned size.

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. I suspect it got reformatted by something.

Comment on lines +890 to +891
assert(PyLong_Check(value));
neg=_PyLong_IsNegative((PyLongObject*)value);
Copy link
Member

Choose a reason for hiding this comment

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

Please put the blank line back.

Suggested change
assert(PyLong_Check(value));
neg=_PyLong_IsNegative((PyLongObject*)value);
assert(PyLong_Check(value));
neg=_PyLong_IsNegative((PyLongObject*)value);


/* Like _PyLong_DigitCount but asserts that op is non-negative */
staticinlinePy_ssize_t
_PyLong_UnsignedDigitCount(constPyLongObject*op)
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 not excited about this name; I keep having to look up how it differs from _PyLong_DigitCount, and it's not really related to _PyLong_SignedDigitCount. :-( Maybe _PyLong_NonNegativeDigitCount? Or perhaps better _PyLong_DigitCountOfNonNegative?

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

I needed this for the extra check during implementation. _PyLong_UnsignedDigitCount is now the same as _PyLong_DigitCount, and should remain so.

I'll remove it.

Comment on lines 510 to 511
* care (see comment above).
*/
Copy link
Member

Choose a reason for hiding this comment

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

Accidental reformat?

Suggested change
*care (seecommentabove).
*/
*care (seecommentabove).
*/

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

Fixed.

} while(0)

for (i=Py_SIZE(b) -1; i >= 0; --i){
for (i=_PyLong_SignedDigitCount(b) -1; i >= 0; --i){
Copy link
Member

Choose a reason for hiding this comment

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

Mybe we can prove that b is nonnegative here?

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

Maybe.
I'm not going to change any algorithms in this PR.

Hopefully, the more explicit semantics of the new API will allow someone to make some improvements in a future PR.

shift=bsize >> 1;
if (kmul_split(a, shift, &ah, &al) <0) goto fail;
assert(Py_SIZE(ah) >0); /* the split isn't degenerate */
assert(_PyLong_UnsignedDigitCount(ah) >0); /* the split isn't degenerate */
Copy link
Member

Choose a reason for hiding this comment

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

This should just check the sign, right?

Suggested change
assert(_PyLong_UnsignedDigitCount(ah)>0); /* the split isn't degenerate */
assert(_PyLong_IsPositive(ah)); /* the split isn't degenerate */

Same below several occurrences.

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

Done

staticinlinebool
_PyLong_IsPositive(constPyLongObject*op)
{
return (op->long_value.lv_tag&SIGN_MASK) ==0;
Copy link
Member

Choose a reason for hiding this comment

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

Sure, fine. Next question: maybe we also need a _PyLong_IsNonZero? I see !_PyLong_IsZero a lot, and the ! is easily missed. (Or maybe that's just my old eyes.) Possibly also IsNonNegative and IsNonPositive.

returnNULL;

memset(z->long_value.ob_digit, 0, Py_SIZE(z) *sizeof(digit));
memset(z->long_value.ob_digit, 0, _PyLong_UnsignedDigitCount(z) *sizeof(digit));
Copy link
Member

Choose a reason for hiding this comment

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

Since z was just created with a nonnegative size:

Suggested change
memset(z->long_value.ob_digit, 0, _PyLong_UnsignedDigitCount(z) *sizeof(digit));
memset(z->long_value.ob_digit, 0, _PyLong_DigitCount(z) *sizeof(digit));

Copy link
Member

@gvanrossumgvanrossum left a comment

Choose a reason for hiding this comment

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

Go for it!

@markshannonmarkshannon merged commit 7559f5f into python:mainMar 22, 2023
@markshannonmarkshannon deleted the long-rearrange-size-bits branch March 22, 2023 14:50
@sobolevn
Copy link
Member

I've opened #102940 to fix two new warnings from this PR :)

Fidget-Spinner pushed a commit to Fidget-Spinner/cpython that referenced this pull request Mar 27, 2023
…2464) * Eliminate all remaining uses of Py_SIZE and Py_SET_SIZE on PyLongObject, adding asserts. * Change layout of size/sign bits in longobject to support future addition of immortal ints and tagged medium ints. * Add functions to hide some internals of long object, and for setting sign and digit count. * Replace uses of IS_MEDIUM_VALUE macro with _PyLong_IsCompact().
@scoder
Copy link
Contributor

ISTM that the simple accessor functions like IsZero, IsPositive, IsNegative should be publicly available.

What about this part? Looks like it was dropped on the floor along the way.

Comment on lines +112 to +113
* 0-1: Sign bits value = (1-sign), ie. negative=2, positive=0, zero=1.
* 2: Reserved for immortality bit
Copy link
Contributor

@eduardo-elizondoeduardo-elizondoApr 7, 2023

Choose a reason for hiding this comment

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

I don't think we need an immortality flag here, but we do need a static flag (immortality should be marked by the refcount and this marks if the object is static or not. Using this, we can do the static check at dealloc time to prevent the deallocation of the objects

staticinlineint
_PyLong_IsNonNegativeCompact(constPyLongObject*op){
assert(PyLong_Check(op));
returnop->long_value.lv_tag <= (1 << NON_SIZE_BITS);
Copy link
Contributor

@eduardo-elizondoeduardo-elizondoApr 7, 2023

Choose a reason for hiding this comment

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

This doesn't work if we set the second (immortal/static) bit, i.e: the immortal small int 1 since it will have an lv_tag of 1100 and return an incorrect value here.

I'll create a new PR to restructure this a bit to make it work with the new bit flag.

cc @ericsnowcurrently

warsaw pushed a commit to warsaw/cpython that referenced this pull request Apr 11, 2023
…2464) * Eliminate all remaining uses of Py_SIZE and Py_SET_SIZE on PyLongObject, adding asserts. * Change layout of size/sign bits in longobject to support future addition of immortal ints and tagged medium ints. * Add functions to hide some internals of long object, and for setting sign and digit count. * Replace uses of IS_MEDIUM_VALUE macro with _PyLong_IsCompact().
CAUTION: Generic code manipulating subtypes of PyVarObject has to
aware that ints abuse ob_size's sign bit.
*/
Copy link
Contributor

@verhovskyverhovskyApr 29, 2023

Choose a reason for hiding this comment

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

You didn't update this comment that documents _longobject, it's still talking about ob_size and PyVarObject

/* Long integer representation. The absolute value of a number is equal to SUM(for i=0 through abs(ob_size)-1) ob_digit[i] * 2**(SHIFT*i) 

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants

@markshannon@scoder@mdickinson@lpereira@sobolevn@gvanrossum@eduardo-elizondo@verhovsky@bedevere-bot