考虑以下代码,这是基于当前时间生成整数标识符的一种幼稚方法,其中 Result
为int64:
dtRef := Now;
Result := YearOf(dtRef) * 100000000000 +
MonthOf(dtRef) * 1000000000 +
DayOf(dtRef) * 10000000 +
HourOf(dtRef) * 100000 +
MinuteOf(dtRef) * 1000 +
SecondOf(dtRef) * 10 +
m_nLastTaskID;
例如,今天生成的ID给出了20190503163412142(< 2^55(,该ID在INT64(2^63-1(范围内。
但是,这给柏林和里约热内卢都带来了整数溢出。它编译为:
MyUnitU.pas.580: Result := YearOf(dtRef) * 100000000000 +
007BCEAE 6A17 push $17
007BCEB0 6800E87648 push $4876e800
007BCEB5 FF75EC push dword ptr [ebp-$14]
007BCEB8 FF75E8 push dword ptr [ebp-$18]
007BCEBB E84CF2D3FF call YearOf
007BCEC0 0FB7C0 movzx eax,ax
007BCEC3 33D2 xor edx,edx
007BCEC5 E8FA0AC5FF call @_llmulo
007BCECA 7105 jno $007bced1
007BCECC E83FC7C4FF call @IntOver
007BCED1 52 push edx
007BCED2 50 push eax
007BCED3 FF75EC push dword ptr [ebp-$14]
007BCED6 FF75E8 push dword ptr [ebp-$18]
007BCED9 E852F2D3FF call MonthOf
007BCEDE 0FB7C0 movzx eax,ax
007BCEE1 BA00CA9A3B mov edx,$3b9aca00
007BCEE6 F7E2 mul edx
007BCEE8 7105 jno $007bceef
007BCEEA E821C7C4FF call @IntOver
第一个乘法使用_llmulo
(64位签名的乘数,带有溢流检查(,而第二次使用平原mul
。以下是我评论的第二个乘法:
007BCED9 E852F2D3FF call MonthOf // Put the month (word) on AX
007BCEDE 0FB7C0 movzx eax,ax // EAX <- AX as a double word
007BCEE1 BA00CA9A3B mov edx,$3b9aca00 // EDX <- 1000000000
007BCEE6 F7E2 mul edx // EDX:EAX <- EAX * EDX
007BCEE8 7105 jno $007bceef // Jump if overflow flag not set
007BCEEA E821C7C4FF call @IntOver // Called (overflow flag set!)
我认为_llmulo
上的溢出标志是因为此错误报告了_llmulo
的问题(还对源表示溢出检查不起作用(。
但是,在调试溢流标志时,实际上是在mul
之后设置的!根据英特尔的手册:
如果结果的上半部分为0,则将CF标志设置为0;否则,将它们设置为1。
在这种情况下,EDX
是0x2A05F200
,EAX
是mul
之后的0x00000001
,因此似乎已经设置了标志的标志。问题是,mul
在这里正确吗?这是编译器的问题还是我的代码问题?
我注意到,如果我将MonthOf
等结果归因于INT64变量,请在乘以1000 ...,所有乘法都使用_llmulo
完成,并且效果很好。
mul
's/cf输出并不意味着64位结果的溢出;这不可能。这意味着结果不适合低32,这对于使用具有特殊情况的代码可能很有用,该结果适用于适合一个寄存器的数字。如果32位mul
产生edx=0x2A05F200
,则是的,上半部不零,因此CF = = 1是正确的。(顺便说一句,https://www.felixcloutier.com/x86/mul具有Intel's Vol.2 PDF的HTML提取物(。
如果Delphi像C,100000000000
已经是INT64,因为它太大了,无法安装32位整数。因此,编译器执行64x64 => 64位乘法,检查64位结果的溢出。
,但是1000000000
dip 适合32位整数,因此您的源正在执行32 x 32 => 32位乘数乘数,并且编译器为正确检查 32位结果的溢出在促进该32位结果至64的添加中,以增加其他64位结果。
我注意到,如果我将月的结果等归因于int64变量,则在乘以1000 ...,所有乘法都使用_llmulo完成,并且效果很好。
好吧,这是一个错过的优化,也许通过优化启用了编译器,该编译器实际上可以简化后期的64x64 => 64乘以32x32 => 64,仅使用mul
或imul
,因为已知输入是狭窄的。/p>
c编译器进行此优化,例如(在Godbolt上(
uint64_t foo_widen(uint32_t a, uint32_t b) {
return a * (uint64_t)b;
}
# gcc9.1 -m32 -O3
foo_widen:
mov eax, DWORD PTR [esp+8] # load 2nd arg
mul DWORD PTR [esp+4] # multiply into EDX:EAX return value
ret
我也首先认为这是我报告的这个编译器错误:
https://quality.embarcadero.com/browse/rsp-16617
系统.__ llmulo返回错误的溢出标志
,但这显然不是Bug (也不是您在QC上找到的delphi 7的错误,该错误是在Delphi 2005中修复的(。无论如何,该错误是在Delphi 10.2 Tokyo中修复的。
如果要避免乘法上的溢出,则将常数施加到Int64
。这也会自动将其他操作数扩大到Int64
,以确保整个中间结果为64位:
Result := YearOf(dtRef) * Int64(100000000000) +
MonthOf(dtRef) * Int64(10000000000) +
etc...
对于旧版本的Delphi,您还应关闭溢出检查:
{$UNDEF OVFL}
...
{$IFOPT Q+} {$DEFINE OVFL} {$Q-} {$ENDIF}
Result := YearOf(dtRef) * Int64(100000000000) +
MonthOf(dtRef) * Int64(10000000000) +
etc...
{$IFDEF OVFL} {$Q+} {$UNDEF OVFL} {$ENDIF}