在64位

时间:2016-01-30 10:35:50

标签: delphi x86-64 basm

对于我的BigInteger代码,对于非常大的BigIntegers,输出结果很慢。所以现在我使用递归的分而治之算法,它仍然需要2'30“才能将当前最大的已知素数转换为超过2200万位数的十进制字符串(但只有135 ms将其转换为十六进制字符串)

我仍然希望减少时间,因此我需要一个例程,可以将NativeUInt(即32位平台上的UInt32,64位平台上的UInt64)除以100非常快。所以我使用乘法乘法。这在32位代码中工作正常,但我对64位不是100%肯定。

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

如果所选参数错误(但很接近),我注意到DivMod100()总是因$4000004B之类的值而失败。是否有特殊值或范围来检查64位,所以我不必检查所有值?

我目前的代码:

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}

3 个答案:

答案 0 :(得分:2)

与编写优化代码时一样,使用编译器输出提示/起始点。在一般情况下,假设它所做的任何优化是安全的,这是安全的。错误的代码编译器错误很少见。

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

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

请参阅注释以分析它实际上在做什么:首先除以4,因此它可以使用一个常数,当除数足够小时,该常数仅用于除以25。这也可以避免在以后需要添加。

#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;
}

compiles to (gcc 5.3 -O3 -mtune=haswell)

    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

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

乘以100的部分。

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

你的评论都说他们选择的版本更快。如果是这样,那是因为一些微妙的指令对齐或其他次要影响:在Intel SnB系列上,它是the same number of uops (3),并且相同的关键路径延迟(mov imm是离开关键路径,mul是3个周期。

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

imul有效,因为you're only using the low 64b of the resultmul没有2或3的操作数形式,因为无论输入的有符号或无符号解释如何,结果的低半部分都是相同的。

顺便说一句,与-march=native一起使用mulx使用mul表示64x64-> 128,而不是mul,但它并没有获得任何收益。根据Agner Fog的表格,它的延迟比imul r,r,i差一个周期。

AMD -mtune=haswell(特别是64b版本)的延迟时间比3c差,这也许是gcc避免它的原因。 IDK gcc维护者在调整成本方面的工作量是多少,因此像-mtune这样的设置运行良好,但很多的代码没有使用任何-march设置编译(甚至一个由imul r64, r64, imm暗示,所以当gcc做出最适合旧CPU或AMD的选择时,我并不感到惊讶。

clang仍然使用-mtune=bdver1和{{1}}(Bulldozer),这可以节省m-ops,但是比使用lea / lea / shl要花费1c的延迟。 (规模&gt; 1的lea在Bulldozer上是2c延迟)。

答案 1 :(得分:1)

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

  example_with_joins = CycleDay.joins(:user).where(users: { role: 'admin' })

答案 2 :(得分:1)

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

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

    1. 舍入到65位;这种舍入的执行方式有一些魔力;通过对Rudy的答案进行逆向工程,正确的舍入是:
    2. 1010 0011 1101 0111 0000 1010 0011 1101 0111 0000 1010 0011 1101 0111 0000 1010 1

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

        1. 以十六进制形式书写(获取报复常数):
        2. A = 47 AE 14 7A E1 47 AE 15

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