对于我的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} |
- 这几乎是对stackoverflow.com/questions/20270596的欺骗,但是在任何情况下,您都可以通过阅读libdivide找到答案
-
我使用libdivide生成了一个常数,但它等于$1C0000000000000000 div 100 + 1且后移位为6,但大部分结果不是n div 100。 libdivide给出了我期望的32位结果,但也许我不明白64位的使用方法。我会做更多的实验。
-
请提供答案。
-
@DavidHeffernan:好的,我找到了使用libdivide.h正确执行操作的方法。显然,需要进行移位/添加步骤。现在工作正常。我应该发布解决方案作为答案,还是只编辑问题?
-
确定,将发布答案。
通常在编写优化代码时,将编译器输出用作提示/起点。一般情况下,可以假定所做的任何优化都是安全的。错误代码的编译器错误很少见。
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延迟)。
-
@ user246408:如果我对您的答案的评论正确无误,那么clang和gcc可能是这样做的,因此他们不需要65位的加法和移位,对吗?看起来确实比Rudy的代码便宜。
-
抱歉,我已删除我的评论;因为我对gcc代码没有足够的了解;是的,它仅使用移位,而不使用加法和移位;常数0x28f5c28f5c28f5c3等于倒数的最高有效位的1/25(或1/100,它们是相同的),仅右移2位。给定gcc代码正确,它基于以下事实使用优化:将右移2位后的股息小于0x4000000000000000。总结:尽管不存在用于除以25的通用64位常量,但对于小于0x4000000000000000的股息存在。
-
@ user246408:感谢您的分析。我没有花时间去看整个图片,我只是想贡献一个"看看编译器做什么"的答案,而不花时间去理解编译器为什么能够做到这一点。
-
如果为div_by_25添加gcc代码,那就太好了。如果我的分析是正确的,则它应该与div_by_100不同,因为需要进行加法和移位操作。
-
@ user246408:godbolt使任何人去尝试代码更改和查看我复制/粘贴的相同asm输出都变得非常容易。这就是为什么我在所有的asm输出答案中都添加了godbolt链接的原因。无论如何,这是div_by_100和div_by_25的链接。您是对的,它会移动一个并加。它使用的常量是0x47ae147ae147ae15,以免将Godbolt翻转为" binary "模式以获取十六进制的麻烦。有趣的是,ICC13对div_by_100和div_by_25都使用该常数。而不是乘以25或100,而是乘以-25或-100。
@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)
-
我实际上为32位做了类似的事情,但是更简单。我将$100000000除以100(实际上是先从div 25开始,然后是shr 2),然后对所有基数进行尝试。当那没有成功时,我使用了$200000000和另外1个班次,并重复了这一过程,直到找到需要的东西为止。
-
FWIW,使用的舍入似乎很简单:向上舍入(从零开始)。但是为什么要删除前导一位呢?我仍然认为magic = $3FFFFFFFFFFFFFFFFF div 100 + 1(您也有这样的东西:$A3D70A3... etc.)和6的偏移也应该起作用。我只是不知道如何可靠地证明或测试它。
-
FWIW,它们的常量是$1C0000000000000000 div 100,我的常量是$400000000000000000 div 100。 $ 1C =28。因此是(28/64)*(64/100)= 28/100。将其加到100/100(X),您将得到X的128/100。向右移动一次,您将得到64/100。向右移动6次,您将获得1/100。 ISTM我的常数($A3D70...)应该在没有加/移位的情况下工作,并给出完全相同的结果。还要注意,64位x 64位永远不会溢出到129位或类似的位,因此不需要像(Q(X-Q)shr 1)这样的技巧。
-
@RudyVelthuis的前1位已删除,因为在最终公式中添加X即可解决该问题;添加X的技巧意味着我们使用65位常量而不是您初次尝试时使用的64位;在64位情况下,64位常量不足以产生始终正确的除法结果;我也不确定在64位情况下65位常量是否足够。
-
嗯...一个32位常量足以可靠地以32位获得正确的结果。也许不是所有的值,但显然是100格。我知道X是加进去的。
-
FWIW,而不是100,我可以使用25进行除法,然后稍后再右移2。那将避免第65位,并允许一个更准确的常数。我必须尝试一下。
-
嗯... libdivide给出相同的常数值和代码,并且只将4而不是6的移位除以25。
-
@RudyVelthuis-看起来65位总是足够的;实际上(N 1)位常数对于N位除法总是足够的,而N位常数仅对某些除数是可能的。
-
我目前正在阅读。这似乎是libdivide代码的基础:gmplib.org/~tege/divcnst-pldi94.pdf
-
@RudyVelthuis-阅读本文后,有些想法:sergworks.wordpress.com/2016/02/01/integer-division-by-const??ant
我用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} |
- 为什么不使用(Q + X) shr 1代替Q + (X - Q) shr 1?
-
嗯...我是从libdivide.h那里拿来的。我猜对于某些值,中间结果可能会溢出。不过我可以尝试。如有必要,我可以尝试使用RCR代替。感谢您的提示。
-
@ user246408:你是对的。在发生溢出的情况下,我使用RCR移回了进位,现在它变得更简单,更快了。我编辑了答案。
-
RCR的有趣技巧将获得额外的中间精度。这是针对Intel SnB系列的3 uop指令(替换了3个1uop insns),因此更改不会在那里保存任何uu。不过,它在AMD上仅为1 m-op,因此可以在其中保存两个宏操作。请注意,RCR的立即数(非1)要慢得多,因此即使您可以将其右移与以下shr结合使用,它也没有用。 RCR是2c延迟(英特尔),mov不在关键路径上(无论如何在IvB和更高版本上都是零延迟),因此这也是一次洗礼。 (sub和shl r,1是1c)
-
@PeterCordes:所以MOV R9,R8; SUB R9,RDX; SHR R9,1; ADD RDX,R9与上面的等效(避免出现"第65位"),等同于上述情况(我知道,它给出的结果相同)?
-
@RudyVelthuis:在Intel SnB系列中,是相同的uop计数和延迟。也许不同的端口要求。 (例如,IvB和更高版本不需要mov端口)。在奔腾-M到Nehalem上,它是2 oups(仍然具有2c的延迟)。在AMD(和PII / PIII)上,add / rcr 1更快。在Silvermont上,rcr 1为7uops(而简单的说明仍为1)。在C语言中,我曾经见过Q + (X - Q)/2习惯用法,用于计算平均值同时避免溢出/进位。无论如何,您偶然发现了另一种"在一个CPU上速度更快,在其他CPU上速度较慢"的情况。
-
您无需在mul之前将rdx设置为零。与div不同,它是mul的只写操作数。就像我在答案中指出的那样,imul rax, rdx, 100比lea / lea / shl更好。 lang甚至使用它。 Clang的版本是9 oups(Intel Haswell)到您的12(不计算您的负载和存储或浪费的xor)。
-
^是的。可能有2种改进:(1)不用将rdx置零,而是将Div100Const移到rdx和MUL RDX中; (2)如果RCR较慢,则将红利1(或2)位右移并除以50(或25)-不会有溢出,因此不需要RCR。
-
@user和Peter:我一直忘记我不必在进行多方处理之前将RDX归零,而只是在开始划分链之前。