乘法整数溢出-编译器错误?

afa*_*rah 4 delphi x86

考虑下面的代码,这是一种基于当前时间生成整数标识符的简单方法,其中ResultInt64:

  dtRef  := Now;

  Result := YearOf(dtRef)   * 100000000000 +
            MonthOf(dtRef)  * 1000000000   +
            DayOf(dtRef)    * 10000000     +
            HourOf(dtRef)   * 100000       +
            MinuteOf(dtRef) * 1000         +
            SecondOf(dtRef) * 10           +
            m_nLastTaskID;
Run Code Online (Sandbox Code Playgroud)

例如,今天生成的ID给出了20190503163412142(<2 ^ 55),这恰好在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

Run Code Online (Sandbox Code Playgroud)

第一个乘法使用_llmulo(64位有符号乘法,带有溢出检查),而第二个乘法使用plain 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!)
Run Code Online (Sandbox Code Playgroud)

我以为是_llmulo由于此错误报告(关于问题的错误报告)而设置了溢出标志_llmulo(在源上也有注释说明溢出检查不起作用)。

但是,在调试时,溢出标志实际上是在mul!之后设置的。根据英特尔的手册

如果结果的上半部分为0,则将OF和CF标志设置为0;否则将其设置为0。否则,它们设置为1。

在这种情况下EDX0x2A05F200EAX0x00000001之后mul,如此看来旗本来应该确实设置。问题是,mul这里正确吗?这是编译器有问题还是我的代码有问题?

我注意到,如果MonthOf在将乘以1000 ...之前将etc 的结果归因于Int64变量,则所有乘法都使用来完成_llmulo,并且工作正常。

Pet*_*des 9

mulOF / CF的输出并不意味着64位结果溢出。这不可能。这意味着结果不适合低32位,这可能对使用特殊结果(对于适合一个寄存器的数字更快)的结果的代码很有用。如果32位mul产生edx=0x2A05F200,则是,上半部不为零,因此CF = OF = 1是正确的。(顺便说一句,https://www.felixcloutier.com/x86/mul 有Intel vol.2 PDF的HTML摘录)。


如果Delphi类似于C, 100000000000则已经是Int64,因为它太大而无法容纳32位整数。因此,编译器执行64x64 => 64位乘法,检查64位结果是否溢出。

但是1000000000 确实适合32位整数,因此您的源代码执行的是32 x 32 => 32位乘法,并且在将32位结果提升为64之前,编译器会正确检查32位结果是否溢出。与其他64位结果相加。

我注意到,如果我将MonthOf等的结果归因于Int64变量,然后再乘以1000 ...,则所有的乘法都使用_llmulo完成,并且工作正常。

好吧,这是一个错过的优化,也许启用了优化功能,编译器会注意到它实际上可以使用just mul或简化后来的64x64 => 64乘以32x32 => 64的乘法imul,因为已知输入是窄的。


C编译器执行此优化,例如(在Godbolt上

uint64_t foo_widen(uint32_t a, uint32_t b) {
    return a * (uint64_t)b;
}
Run Code Online (Sandbox Code Playgroud)
# 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
Run Code Online (Sandbox Code Playgroud)

  • 请参阅以下范围/溢出摘要:[范围检查错误(Delphi)的原因](/sf/answers/816119461/)。它表明,当对&gt; = 32位的操作数进行运算时,编译器将发出溢出错误,而少于32位时,将发出范围错误。 (3认同)

Rud*_*uis 6

我还首先想到的是我报告的此编译器错误:

https://quality.embarcadero.com/browse/RSP-16617

System .__ llmulo返回错误的溢出标志

但这显然不是那个bug(对于在Delphi 7中已修复的Delphi 7,您也不是在QC中发现的bug)。无论如何,该错误已在Delphi 10.2 Tokyo中修复。

如果要避免乘法溢出,则将常量转换为Int64。这也将自动将另一个操作数扩展为Int64,确保整个中间结果将为64位:

Result := YearOf(dtRef)  * Int64(100000000000) + 
          MonthOf(dtRef) * Int64(10000000000) +
          etc...
Run Code Online (Sandbox Code Playgroud)

对于较旧的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}
Run Code Online (Sandbox Code Playgroud)