Summary: The number is not random, it's part of a calculation that ensures proper stack alignment. The number should be 15, and the 22 is the result of a minor bug in old versions of GCC.
Recall that the x86-64 SysV ABI mandates 16-byte stack alignment; the stack pointer must be a multiple of 16 prior to any call
instruction. Hence when we enter read_and_process
, the stack pointer is 8 less than a multiple of 16, because the call
that got us here pushed 8 bytes. So prior to calling read_val()
, the stack pointer must be decremented by 8 more than a multiple of 16, i.e. an odd multiple of 8. The prologue pushes an odd number of registers (five, namely rbp, r14, r13, r12, rbx
) at 8 bytes each. So the remaining stack adjustment must be a multiple of 16.
So whatever amount of memory is to be allocated for the array vals
, it must be rounded up to a multiple of 16. A standard way to do that is to add 15, then AND with -16: adjusted = (orig + 15) & -16
.
Why does that work? -16
, thanks to two's complement arithmetic, has the low 4 bits clear and the others set, so AND with -16
results in a multiple of 16 - but since the AND clears low-order bits, the result of x & -16
is less than x
; this is rounding down. If we add 15 first (which is, of course, 1 less than 16), the net effect is to round up instead. Adding 15 to orig
will cause it to pass a multiple of 16, and then & -16
will round down to that multiple of 16. Unless orig
was already a multiple of 16, in which case orig+15
rounds down back to orig
itself. So this does the right thing in all cases.
That's what GCC does from 8.1.0 onward. Adding 15 is baked into the same lea
that multiplies n
by 8, and the AND with -16
comes several lines later.
In this case, since orig = 8*n
is already a multiple of 8, there are other values besides 15 that would work just as well; 8, for instance (though not 16, see below). But using 15 is completely equivalent mathematically and in terms of code size and speed, and since 15 works regardless of previous alignment, the compiler authors can just use 15 unconditionally without writing extra code to keep track of what alignment orig
may already have.
But adding 22 instead, as older GCC does, is clearly wrong. If orig
was already a multiple of 16, say orig = 32
, then orig+22
is 54, which rounds down to 48. But 32 bytes was already a perfectly good size so we just wasted 16 bytes for no reason. (Here orig
is 8*n
so this would occur if the input n
is even.) For similar reasons, your suggestion of using 16 instead of 22 would also be wrong.
So the 22 is a bug. It's a pretty minor bug; the resulting code still works just fine and complies with the ABI, and the only ill effect is that sometimes a little bit of stack space is wasted. But it was fixed for GCC 8.1.0 by a commit entitled "Improve alloca alignment". (alloca
is an old non-standard function that does dynamic stack allocation, and compiler writers often use the term to refer to any stack allocation.)
Apparently the issue was that some previous pass of the compiler had determined that the size needed to be aligned to (at least) 8 bytes, which would have been accomplished by adding 7 and ANDing with -8 (which might later be optimized away when the compiler realized later that n*8
is already aligned to 8 bytes). Now this constraint should be made redundant when the compiler realizes that 16-byte alignment is actually required, as every multiple of 16 is already a multiple of 8. But the compiler erroneously adds the offsets 7 and 15, when the right thing to do was to take their maximum (which is what the commit implemented). And 7 + 15 is... 22.
If you compile the code using GCC 5.4 with optimizations off, you can see these two operations happening separately:
lea rdx, [rax+7] ; add 7 to rax and write to rdx
mov eax, 16
sub rax, 1 ; now rax = 15
add rax, rdx ; add 15 to rdx
and with optimizations on, the optimizer combines these into a single add of 22 - without noticing that the add of 7 should not have been there to begin with. In newer versions of GCC with -O0
, the lea rdx, [rax+7]
is gone.
c++
andvariable-length-array
are contradictory. I suggest you retag withc
language to have better support (C++ programmers hate VLA) – Odey+6
. Then+16
and AND with-16
is a trick to align on 16-byte boundary (AND -16
clears the 4 lower bits). – Pygidiumn
is already even. 22 is harder to explain, but one note is 22 = 15 + 7, where 7 is one less thansizeof(long)
. I would wonder if the compiler tried to align twice, once up to a multiple of 8 (needless) and then again up to a multiple of 16, and naively combined the additions without noticing it was redundant. That could be a minor bug in GCC 5 that was fixed later. – Malayan0xFF…0
would blow up the lower 4bit so that the bit string would be multiple of 16. But i couldn’t understand why the 15 is added. It would be helpful to write it as an answer, not comment. Thanks. – Armada-16
(which is just another name for0xfffffffffffffff0
) effectively rounds down to a multiple of 16. As to why adding 15 first causes it to round up, try for yourself and see what happens when you apply this to inputs like 64, 65, 66, 78, 79 and 80. – Malayan64-4=60
bits to be changed when it has to be rounded up to next multiple of 16. Otherwise, it retains original multiple of 16 value. By the way, is there a relation in that then
is even or odd? You said that “Adding16
would waste space ifn
is already even” in previous comment. What’s the point then
is even or odd number? Edit) When then
is odd, after ANDing,8n+15
would be8n+8
. Otherwise, when then
is even, after ANDing,8n+15
would be8n
. In either case, the result is multiple of 16. – Armada(x+16)&-16
. Ifn
is even, then8n
is already a multiple of 16 and so need not be increased at all. But(8n+16)&-16 = 8n+16
which is unnecessarily large. That's all I meant. For oddn
,(8n+16)&-16 == 8n+8 == (8n+15)&-16
so 15 versus 16 doesn't make a difference in that case. – Malayanx
is already multiple of8
because it is8n
. Therefore thex
can’t be65
,66
and etc. So, why the expression can’t be(8n+8)&-16
? – Armada8n+8
would work in this case. But 15 has exactly the same effect, has the same cost in execution and code size, and it has the advantage that the compiler can use the same calculation for any array regardless of its element size (e.g. forchar a[n];
then 15 is the only number that would work.) – Malayanceil(x/16)*16
mathematically. But logic the gcc performs isfloor((x+15)/16)*16
. Is there a special reason to perform the work in the latter way? Yes, both expressions are equivalent — i found the proof in here. The latter can be performed as clearing low-order bits. Is it possible to perform the work in the former way? – Armadafloor
is effectively built into the very efficient mask operation:y & -16 == floor(y/16)*16
. This is likewise true for other unsigned integer operations:y << 4 == floor(y/16)
, and even the (much more expensive) general unsigned integer division truncates toward zero. On the other hand, it is not clear how to directly perform aceil
with similar efficiency. By the time you do an integer divide, thefloor
is already done and it's too late toceil
. – Malayanfloor
andceil
is usually unhelpful in integer contexts. You have a fundamentally integer operation, but you're resorting to the real numbers (or at least the rationals) to define it. Computers are great with integers, less good with rationals, and can deal with real numbers only very approximately. – Malayan