bpo-37812: Expand confusing CHECK_SMALL_INT so return is explicit.#15216
bpo-37812: Expand confusing CHECK_SMALL_INT so return is explicit.#15216rhettinger merged 3 commits into
Conversation
It's not obvious when looking at a call site of this macro that it affects the control flow. Move the `return` into the callers so it's explicit there, and factor out the fiddly bit as IS_SMALL_INT. As a bonus, a couple of other spots can also use IS_SMALL_INT with a benefit to clarity.
|
A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated. Once you have made the requested changes, please leave a comment on this pull request containing the phrase |
Sorry, something went wrong.
|
Thanks for the reviews! I have made the requested changes; please review again. |
Sorry, something went wrong.
|
Thanks for making the requested changes! @rhettinger: please review the changes made to this pull request. |
Sorry, something went wrong.
|
Using inlined function generates unnecessary machine code than using macro. The use inlined function: _PyLong_FromLong PROC ; COMDAT
; 317 : {
push ebp
mov ebp, esp
and esp, -8 ; fffffff8H
push ecx
mov ecx, DWORD PTR _ival$[ebp]
; 318 : PyLongObject *v;
; 319 : unsigned long abs_ival;
; 320 : unsigned long t; /* unsigned so >> doesn't propagate sign bit */
; 321 : int ndigits = 0;
; 322 : int sign;
; 323 :
; 324 : if (is_small_int(ival)) {
mov eax, ecx
push ebx
push esi
cdq
push edi
xor edi, edi
; 48 : return -NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS;
add eax, 5
adc edx, edi
test edx, edx
ja SHORT $LN6@PyLong_Fro
jb SHORT $LN32@PyLong_Fro
cmp eax, 261 ; 00000105H
ja SHORT $LN6@PyLong_Fro
$LN32@PyLong_Fro:
; 60 : v = (PyObject *)&small_ints[ival + NSMALLNEGINTS];
movsx eax, cx
shl eax, 4
add eax, OFFSET _small_ints+80
; File d:\dev\cpython\include\object.h
; 459 : op->ob_refcnt++;
inc DWORD PTR [eax]
; File d:\dev\cpython\objects\longobject.c
; 383 : }
pop edi
pop esi
pop ebx
mov esp, ebp
pop ebp
ret 0use macro: _PyLong_FromLong PROC ; COMDAT
; 318 : {
push ebp
mov ebp, esp
mov eax, DWORD PTR _ival$[ebp]
push edi
; 319 : PyLongObject *v;
; 320 : unsigned long abs_ival;
; 321 : unsigned long t; /* unsigned so >> doesn't propagate sign bit */
; 322 : int ndigits = 0;
xor edi, edi
; 323 : int sign;
; 324 :
; 325 : if (IS_SMALL_INT(ival)) {
cmp eax, -5 ; fffffffbH
jl SHORT $LN6@PyLong_Fro
cmp eax, 257 ; 00000101H
jge SHORT $LN6@PyLong_Fro
; 60 : v = (PyObject *)&small_ints[ival + NSMALLNEGINTS];
cwde
shl eax, 4
add eax, OFFSET _small_ints+80
pop edi
; File d:\dev\cpython\include\object.h
; 459 : op->ob_refcnt++;
inc DWORD PTR [eax]
; File d:\dev\cpython\objects\longobject.c
; 384 : }
pop ebp
ret 0 |
Sorry, something went wrong.
Interesting, thanks! It looks like several compilers produce code much like this when compiling for x86-32. Here's a Compiler Explorer example I just created: On the other hand, as you can see in the same Compiler Explorer example (in the upper-right pane), VC2017 compiling for x86-64 produces essentially identical code for the macro version and the static-inline-function version. So does Clang; and in fact, because in my example both are present, GCC notices they're identical and makes the macro version just jump immediately to the function version 😄 I believe the longer code represents a missed optimization by the compilers; I don't think there's any semantic reason why (on any architecture) they have to emit anything different for the function version vs. the macro version. In any case, stepping back: we're talking about a handful of additional instructions, of the kinds a CPU is very fast at (no random reads from memory, which can take thousands of cycles and which the interpreter by its nature is full of); also only in 32-bit builds, not 64-bit. It's interesting to see the code that's generated and study it... but convincing the compiler to generate one of these versions over another is very much a micro-optimization, the kind that generally isn't worth more than a very small delta in the simplicity of the code. For more fun, I just went and added to the Compiler Explorer example a version with With So, switching from |
Sorry, something went wrong.
|
If build a 32-bit version with gcc 9.2, it also generates unnecessary machine code (convert to Same with ARM gcc 8.2: https://godbolt.org/z/qycDzR
|
Sorry, something went wrong.
In this case, the
I know why, on 64-bit Windows, |
Sorry, something went wrong.
No, compilers have a lot more freedom than that. The code just needs to compute the right answer. The types in the source are part of defining what the right answer is, but there doesn't need to be anything at runtime that physically looks like them (in a case like this where they aren't part of any external interface.) Here's a quick demo of that: |
Sorry, something went wrong.
If one day I have no opinion on the use of inlined function or macro here. |
Sorry, something went wrong.
That's true (provided |
Sorry, something went wrong.
|
It seems that all the major compilers have generated unnecessary code. |
Sorry, something went wrong.
It's not obvious when looking at a call site of this macro that it
affects the control flow. Move the
returninto the callers soit's explicit there, and factor out the fiddly bit as IS_SMALL_INT.
As a bonus, a couple of other spots can also use IS_SMALL_INT with a
benefit to clarity.
https://bugs.python.org/issue37812