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



考虑以下代码,这是基于当前时间生成整数标识符的一种幼稚方法,其中 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。

在这种情况下,EDX0x2A05F200EAXmul之后的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,仅使用mulimul,因为已知输入是狭窄的。/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}

相关内容

  • 没有找到相关文章

最新更新