我正在研究很长整数的乘法运算(大约100,000个十进制数字)。作为我的图书馆的一部分,我要添加两个长数字。
分析表明我的代码在add()和sub()例程中运行的时间高达25%,因此尽可能快地运行它们非常重要。但我还没有看到太大的潜力。也许你可以给我一些帮助,建议,见解或想法。我会测试它们并回复你。
到目前为止,我的添加例程进行了一些设置,然后使用了8次展开的循环:
mov rax, QWORD PTR [rdx+r11*8-64]
mov r10, QWORD PTR [r8+r11*8-64]
adc rax, r10
mov QWORD PTR [rcx+r11*8-64], rax
随后会有7个具有不同偏移的块然后循环。
我之前尝试过加载内存中的值,但这没有帮助。我想那是因为好的预取。我使用Intel i7-3770 Ivy Bridge 4核CPU。但我想编写适用于任何现代CPU的代码。
编辑:我做了一些时间:它在大约2.25个周期/单词中增加了1k个单词。如果我移除ADC,那么只剩下MOV,它仍然需要大约1.95个周期/字。所以主要的瓶颈似乎是内存访问。库memcpy()
工作在大约0.65个周期/字,但只有一个输入,而不是两个。不过,我认为它因使用SSE寄存器而快得多。
有些问题:
ADD r11, 8
。我非常感谢任何评论。
答案 0 :(得分:3)
我非常确定memcpy更快,因为它在执行下一个操作之前不会依赖于正在获取的数据。
如果你可以安排你的代码,那么就像这样:
mov rax, QWORD PTR [rdx+r11*8-64]
mov rbx, QWORD PTR [rdx+r11*8-56]
mov r10, QWORD PTR [r8+r11*8-64]
mov r12, QWORD PTR [r8+r11*8-56]
adc rax, r10
adc rbx, r12
mov QWORD PTR [rcx+r11*8-64], rax
mov QWORD PTR [rcx+r11*8-56], rbx
我不是100%确定-56的偏移是适合您的代码的,但概念是“正确的”。
我还会考虑缓存命中/缓存冲突。例如。如果你有三个数据块[你可能会这样做],你要确保它们没有与缓存中的相同偏移对齐。一个不好的例子是,如果您从缓存中的同一位置分配所有块的缓存大小的倍数。过度分配并确保您的不同数据块至少偏移512字节[因此分配4K超大,并向上舍入到4K边界起始地址,然后将512添加到第二个缓冲区,将1024添加到第三个缓冲区]
如果您的数据足够大(大于L2缓存),您可能需要使用MOVNT来获取/存储数据。这将避免读入缓存 - 当你拥有非常大的数据时,这只会带来好处,下一次读取只会导致你可能发现“有用”的其他内容被踢出缓存,你将无法获得在你将它从缓存中踢出之前回到它 - 所以将值存储在缓存中实际上并没有帮助......
编辑:使用SSE或类似功能无济于事,如下所述: Can long integer routines benefit from SSE?
答案 1 :(得分:2)
最困难的依赖是每个内存块之间的进位传播;我试图首先设备一种处理它的方法。
以下片段模拟进位传播,但具有不使用进位标志的“好处”。 此可以并行化为三个或四个单独的线程,每个线程产生一半,相隔大约25000个十进制数字(或10000字节)。那些只影响一个字节,字,双字等的载荷的概率将渐近地达到零。
long long carry=0;
for (int i=0;i<N;i++) {
carry += (long long)*a++ + (long long)*b++;
*c++ = carry; carry>>=32;
}
根据我的分析,使用xmm进行无负载添加需要~550ms(1e9字), 模拟进位需要~1020ms,4路并行版本需要~820ms(没有任何汇编程序优化)。
架构优化可能包括使用冗余数字系统,其中进位不必一直传播,并且进位的评估几乎可以无限延迟。
答案 2 :(得分:1)
首先尝试预取数据(您可以先尝试将更多数据块读取到x64寄存器然后进行计算),检查数据是否在内存中正确对齐,将循环代码放在标签对齐的16处,尝试删除SIB寻址
您还可以尝试将代码缩短为:
mov rax, QWORD PTR [rdx+r11*8-64]
adc rax, QWORD PTR [r8+r11*8-64]
mov QWORD PTR [rcx+r11*8-64], rax