I use simple circular buffer like this
var
Values: array [byte] of single;
ptr: byte;
In this test example
for ptr:=0 to 10 do Values[Byte(ptr-5)]:=1;
I expect to have set to 1 first 5 values and last 5 values, but XE4 compiller produce incorrect code, its using 32bit pointer math to calculate array index:
for ptr:=0 to 10 do Values[Byte(ptr-5)]:=1;
005B94BB C645FB00 mov byte ptr [ebp-$05],$00
005B94BF 33C0 xor eax,eax
005B94C1 8A45FB mov al,[ebp-$05]
005B94C4 C78485E0FBFFFF0000803F mov [ebp+eax*4-$0420],$3f800000
005B94CF FE45FB inc byte ptr [ebp-$05]
005B94D2 807DFB0B cmp byte ptr [ebp-$05],$0b
005B94D6 75E7 jnz $005b94bf
Is it my wrong code and whats proper way to operate byte indexes?
The question is:
Is a wrap expected within the
Byte()
cast?
Lets compare the disassembly with overflow checking on/off.
{$Q+}
Project71.dpr.21: for ptr:= 0 to 10 do Values[Byte(ptr-5)]:= 1;
0041D568 33DB xor ebx,ebx
0041D56A 0FB6C3 movzx eax,bl
0041D56D 83E805 sub eax,$05
0041D570 7105 jno $0041d577
0041D572 E82D8DFEFF call @IntOver
0041D577 0FB6C0 movzx eax,al
0041D57A C704870000803F mov [edi+eax*4],$3f800000
0041D581 43 inc ebx
0041D582 80FB0B cmp bl,$0b
0041D585 75E3 jnz $0041d56a
{$Q-}
Project71.dpr.21: for ptr:= 0 to 10 do Values[Byte(ptr-5)]:= 1;
0041D566 B30B mov bl,$0b
0041D568 B808584200 mov eax,$00425808
0041D56D C7000000803F mov [eax],$3f800000
0041D573 83C004 add eax,$04
0041D576 FECB dec bl
0041D578 75F3 jnz $0041d56d
With {$Q+}
the wraps works, while with {$Q-}
the wrap does not work and the compiler does not generate a range error for the wrong array indexing when {$R+}
is set.
So, to me the conclusion is: Since the range check on
does not generate a run-time error for an array index out of bounds, a wrap is expected.
This is further proved by the fact that a wrap is done when overflow checking is on.
This should be reported as a bug in the compiler.
Done: https://quality.embarcadero.com/browse/RSP-15527 "Type cast fail within array indexing"
Note: a workaround is given by @Rudy in his answer.
Addendum:
Following code:
for ptr:= 0 to 10 do WriteLn(Byte(ptr-5));
generates:
251
252
253
254
255
0
1
2
3
4
5
for all combinations of range/overflow checking.
Likewise Values[Byte(-1)] := 1;
assigns 1 to Values[255] for all compiler options.
The documentation for Value Typecasts says:
The resulting value is obtained by converting the expression in parentheses. This may involve truncation or extension if the size of the specified type differs from that of the expression. The expression's sign is always preserved.
My code is written in Delphi 10.1 Berlin, but the result seems to be the same.
Let's extend your little code piece a little:
procedure Test;
var
Values: array[Byte] of Single;
Ptr: byte;
begin
Values[0] := 1.0;
for Ptr := 0 to 10 do
Values[Byte(Ptr - 5)] := 1.0;
end;
This gives the following code in the CPU view:
Project80.dpr.15: Values[0] := 1.0;
0041A1DD C785FCFBFFFF0000803F mov [ebp-$00000404],$3f800000
Project80.dpr.16: for Ptr := 0 to 10 do
0041A1E7 C645FF00 mov byte ptr [ebp-$01],$00
Project80.dpr.17: Values[Byte(Ptr-5)] := 1.0;
0041A1EB 33C0 xor eax,eax
0041A1ED 8A45FF mov al,[ebp-$01]
0041A1F0 C78485E8FBFFFF0000803F mov [ebp+eax*4-$0418],$3f800000
0041A1FB FE45FF inc byte ptr [ebp-$01]
Project80.dpr.16: for Ptr := 0 to 10 do
0041A1FE 807DFF0B cmp byte ptr [ebp-$01],$0b
0041A202 75E7 jnz $0041a1eb
As we can see, the first element of the array is at [ebp-$00000404]
, so [ebp+eax*4-$0418]
is indeed below the array (for values 0..4).
That looks like a bug to me, because for Ptr = 0
, Byte(Ptr - 5)
should wrap around to $FB
. The generated code should be something like:
mov byte ptr [ebp-$01],$00
xor eax,eax
@loop:
mov al,[ebp-$01]
sub al,5 // Byte(Ptr - 5)
mov [ebp+4*eax-$0404],$3f800000 // al = $FB, $FC, $FD, $FE, $FF, 00, etc..
inc byte ptr [ebp-$01]
cmp byte ptr [ebp-$01],$0b
jnz @loop
Good find!
There is a workaround, though:
Values[Byte(Ptr - 5) + 0] := 1.0;
This produces:
Project80.dpr.19: Values[Byte(Ptr - 5) + 0] := 1.0;
0040F16B 8A45FF mov al,[ebp-$01]
0040F16E 2C05 sub al,$05
0040F170 25FF000000 and eax,$000000ff
0040F175 C78485FCFBFFFF0000803F mov [ebp+eax*4-$0404],$3f800000
And that works nicely, although the and eax,$000000ff
seems unnecessary to me.
FWIW, I also looked at the code generated with optimization on. Both in XE and Berlin, the error exists as well, and the workaround works too.
Sounds like an unexpected behavior of the compiler. But I never assume that transtyping integers using byte()
would always make a rounding around $ff
. It does, most of the time, e.g. if you assign values between variables, but there are cases where it doesn't - as you discovered. So I would have never used this byte()
expression within an array index computation.
I always observed that using byte
variables is not worth it, and you should rather use plain integer
(or NativeInt
), so that it matches the CPU registers, and then don't assume any complex rounding.
In all cases, I would rather make the 255 rounding explicit, as such:
procedure test;
var
Values: array [byte] of single;
ptr: integer;
begin
for ptr:=0 to 10 do Values[(ptr-5) and high(Values)]:=1;
end;
As you can see, I've made some modifications:
for
loop index as an integer, to use a CPU register;and
operation for fast binary rounding (writing (ptr-5) mod 256
would be much slower);high(Values)
instead of fixed $ff
constant, which indicates where this rounding comes from.Then the generated code is quick and optimized:
TestAll.dpr.114: begin
0064810C 81C400FCFFFF add esp,$fffffc00
TestAll.dpr.115: for ptr:=0 to 10 do Values[(ptr-5) and high(Values)]:=1;
00648112 33C0 xor eax,eax
00648114 8BD0 mov edx,eax
00648116 83EA05 sub edx,$05
00648119 81E2FF000000 and edx,$000000ff
0064811F C704940000803F mov [esp+edx*4],$3f800000
00648126 40 inc eax
00648127 83F80B cmp eax,$0b
0064812A 75E8 jnz -$18
TestAll.dpr.116: end;
0064812C 81C400040000 add esp,$00000400
00648132 C3 ret
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With