关于delphi:通过64位常数检查乘法参数

Checking parameters of multiplication by constant in 64 bit

对于我的BigInteger代码,对于非常大的BigIntegers而言,输出速度很慢。因此,现在我使用递归分治算法,该算法仍需要2 \\ '30 "才能将当前最大的已知质数转换为超过2200万个数字的十进制字符串(但只需135 ms即可将其转换为十六进制)字符串)。

我仍然想减少时间,因此我需要一个例程,可以将NativeUInt(即32位平台上的UInt32,64位平台上的UInt64)快速除以100。所以我用常数乘。这可以在32位代码中正常工作,但是我不确定100%是否适用于64位。

所以我的问题是:对于无符号的64位值,有没有办法检查乘以常数的结果的可靠性?我通过简单地尝试UInt32的所有值(0 .. $ FFFFFFFF)来检查了32位值。这花了大约。 3分钟。检查所有UInt64将花费比我一生更长的时间。有没有办法检查所使用的参数(常数,移位后)是否可靠?

我注意到,如果选择的参数错误(但关闭),对于$4000004B这样的值,DivMod100()总是失败。是否需要检查64位的特殊值或范围,所以我不必检查所有值?

我当前的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
const
{$IF DEFINED(WIN32)}
  // Checked
  Div100Const = UInt32(UInt64($1FFFFFFFFF) div 100 + 1);
  Div100PostShift = 5;
{$ELSEIF DEFINED(WIN64)}
  // Unchecked!!
  Div100Const = $A3D70A3D70A3D71;
  // UInt64(UInt128($3 FFFF FFFF FFFF FFFF) div 100 + 1);
  // UInt128 is fictive type.
  Div100PostShift = 2;
{$IFEND}

// Calculates X div 100 using multiplication by a constant, taking the
// high part of the 64 bit (or 128 bit) result and shifting
// right. The remainder is calculated as X - quotient * 100;
// This was tested to work safely and quickly for all values of UInt32.
function DivMod100(var X: NativeUInt): NativeUInt;
{$IFDEF WIN32}
asm
        // EAX = address of X, X is UInt32 here.
        PUSH    EBX
        MOV     EDX,Div100Const
        MOV     ECX,EAX
        MOV     EAX,[ECX]
        MOV     EBX,EAX
        MUL     EDX
        SHR     EDX,Div100PostShift
        MOV     [ECX],EDX       // Quotient

        // Slightly faster than MUL

        LEA     EDX,[EDX + 4*EDX] // EDX := EDX * 5;
        LEA     EDX,[EDX + 4*EDX] // EDX := EDX * 5;
        SHL     EDX,2             // EDX := EDX * 4; 5*5*4 = 100.

        MOV     EAX,EBX
        SUB     EAX,EDX         // Remainder
        POP     EBX
end;
{$ELSE WIN64}
asm
        .NOFRAME

        // RCX is address of X, X is UInt64 here.
        MOV     RAX,[RCX]
        MOV     R8,RAX
        XOR     RDX,RDX
        MOV     R9,Div100Const
        MUL     R9
        SHR     RDX,Div100PostShift
        MOV     [RCX],RDX      // Quotient

        // Faster than LEA and SHL

        MOV     RAX,RDX
        MOV     R9D,100
        MUL     R9
        SUB     R8,RAX
        MOV     RAX,R8         // Remainder
end;
{$ENDIF WIN32}


通常在编写优化代码时,将编译器输出用作提示/起点。一般情况下,可以假定所做的任何优化都是安全的。错误代码的编译器错误很少见。

gcc使用常数0x28f5c28f5c28f5c3实现无符号的64位divmod。我没有详细研究如何生成除法常数,但是有一些算法可以产生已知的良好结果(因此不需要详尽的测试)。

该代码实际上有一些重要的区别:它使用的常量与OP的常量不同。

请参阅注释以分析其实际作用:首先除以4,因此它可以使用一个常数,该常数仅在股息足够小时除以25。这也避免了以后再添加任何内容。

1
2
3
4
5
6
7
8
#include <stdint.h>

// rem, quot ordering takes one extra instruction
struct divmod { uint64_t quotient, remainder; }
 div_by_100(uint64_t x) {
    struct divmod retval = { x%100, x/100 }
;
    return retval;
}

编译为(gcc 5.3 -O3 -mtune=haswell):

1
2
3
4
5
6
7
8
9
10
11
12
    movabs  rdx, 2951479051793528259
    mov     rax, rdi            ; Function arg starts in RDI (SysV ABI)
    shr     rax, 2
    mul     rdx
    shr     rdx, 2
    lea     rax, [rdx+rdx*4]    ; multiply by 5
    lea     rax, [rax+rax*4]    ; multiply by another 5
    sal     rax, 2              ; imul rax, rdx, 100 is better here (Intel SnB).
    sub     rdi, rax
    mov     rax, rdi
    ret
; return values in rdx:rax

使用" binary"选项以十六进制形式查看常量,因为反汇编程序输出就是这样做的,这与gcc的asm源输出不同。

乘以100的部分。

gcc使用lea / lea / shl的上述顺序,与您的问题相同。您的答案是使用mov imm / mul序列。

您的每个评论都说他们选择的版本更快。如果是这样,那是由于某种微妙的指令对齐或其他次要效果:在Intel SnB系列上,它具有相同的uops(3)数量,并且具有相同的关键路径延迟(mov imm偏离了关键路径,而mul是3个周期)。

clang使用我认为是最好的选择(imul rax, rdx, 100)。在我看到c选择它之前,我已经想到了这一点,那并不重要。那是1个融合域uop(只能在p0上执行),但延迟为3c。因此,如果使用此例程进行多精度操作受延迟限制,那么它可能无济于事,但这是最佳选择。 (如果您受延迟限制,那么将代码内联到循环中而不是通过内存传递参数之一可以节省很多周期。)

imul之所以有效,是因为您仅使用结果的低64b。 mul没有2或3操作数形式,因为无论输入的有符号或无符号解释,结果的下半部分都是相同的。

BTW,带有-march=native的叮当声将mulx用于64x64-> 128,而不是mul,但不会获得任何好处。根据Agner Fog的表,这比mul延迟了一个周期。

对于imul r,r,i(尤其是64b版本),AMD的延迟低于3c,这也许就是gcc避免使用它的原因。 IDK gcc维护人员需要投入多少工作来调整成本,因此-mtune=haswell这样的设置可以很好地工作,但是很多代码都没有使用任何-mtune设置编译(甚至是-march所隐含的设置),所以我并不感到惊讶当gcc做出最适合较旧CPU或AMD的选择时。

clang仍将imul r64, r64, imm-mtune=bdver1(Bulldozer)一起使用,这可以节省m-op,但与使用lea / lea / shl相比,延迟时间为1c。 (标度> 1的lea是推土机上的2c延迟)。


@Rudy答案中的代码来自以下步骤:

  • 以二进制形式写1/100:0.000000(10100011110101110000);
  • 计算小数点后的前导零:S = 6;
  • 72个第一有效位是:
  • 1010 0011 1101 0111 0000 1010 0011 1101 0111 0000 1010 0011 1101 0111 0000 1010 0011 1101

  • 四舍五入至65位;如何进行四舍五入有某种魔术;通过对Rudy答案的常量进行逆向工程,正确的舍入为:
  • 1010 0011 1101 0111 0000 1010 0011 1101 0111 0000 1010 0011 1101 0111 0000 1010 1

  • 删除前导1位:
  • 0100 0111 1010 1110 0001 0100 0111 1010 1110 0001 0100 0111 1010 1110 0001 0101

  • 以十六进制形式编写(取回修改后的常数):
  • A = 47 AE 14 7A E1 47 AE 15

  • X div 100 = (((uint128(X) * uint128(A)) shr 64) + X) shr 7 (7 = 1 + S)

  • 我用libdivide.h找到了解决方案。这是Win64稍微复杂一点的部分:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    {$ELSE WIN64}
    asm
            .NOFRAME

            MOV     RAX,[RCX]
            MOV     R8,RAX
            XOR     RDX,RDX
            MOV     R9,Div100Const       // New: $47AE147AE147AE15
            MUL     R9                   // Preliminary result Q in RDX

            // Additional part: add/shift

            ADD     RDX,R8               // Q := Q + X shr 1;
            RCR     RDX,1

            SHR     RDX,Div100PostShift  // Q := Q shr 6;
            MOV     [RCX],RDX            // X := Q;

            // Faster than LEA and SHL

            MOV     RAX,RDX
            MOV     R9D,100
            MUL     R9
            SUB     R8,RAX
            MOV     RAX,R8         // Remainder
    end;
    {$ENDIF WIN32}