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

时间:2019-05-03 22:03:42

标签: delphi x86

考虑以下代码,这是一种基于当前时间的整数标识符的幼稚方法,其中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),该值完全在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!)

我以为this bug report_llmulo的问题上已在_llmulo上设置了溢出标志(在源上也有一条评论指出,溢出检查不起作用)。

但是,在调试时,实际上在mul之后设置了溢出标志!根据{{​​3}}:

  

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

在这种情况下,{{1}之后的EDX0x2A05F200,而EAX0x00000001,因此似乎确实应该设置OF标志。问题是mul在这里正确吗?这是编译器有问题还是我的代码有问题?

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

2 个答案:

答案 0 :(得分:9)

mul的OF / 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位结果是否溢出,然后将该32位结果提升为64,以便与其他64位结果相加。

  

我注意到,如果我在将MonthOf等的结果乘以1000之前将其归因于Int64变量,则所有的乘法都使用_llmulo完成,并且效果很好。

这是一个错过的优化,也许启用了优化,编译器会注意到,它实际上可以仅使用mulimul来简化后来的64x64 => 64乘以32x32 => 64,因为输入是狭窄的。


C编译器执行此优化,例如(on 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

答案 1 :(得分:6)

我还首先认为这是我报告的此编译器错误:

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

  

系统。__llmulo返回错误的溢出标志

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

如果要避免乘法溢出,请将常量转换为Options -MultiViews RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^ index.html [QSA,L] 。这也会自动将另一个操作数扩展为Int64,确保整个中间结果为64位:

Int64

对于较早版本的Delphi,您还应该关闭溢出检查:

Result := YearOf(dtRef)  * Int64(100000000000) + 
          MonthOf(dtRef) * Int64(10000000000) +
          etc...