Optimization 优化x64汇编程序多循环

Optimization 优化x64汇编程序多循环,optimization,assembly,x86-64,multiplication,gmp,Optimization,Assembly,X86 64,Multiplication,Gmp,我正在写数学代码,它需要快速乘法大数。它分解为整数数组与单个整数的乘法。在C++中,这看起来像(在无符号的)上: 当我对此进行基准测试时,我发现在我的Core2 Quad上,每次乘法大约需要6.3个周期 我的问题是:我能不能加快速度?不幸的是,我认为没有办法避免其中一个加法,乘法总是需要RDX:RAX,所以我需要移动数据,不能进行“并行乘法” 有什么想法吗 更新: 经过更多的测试,我已经设法将速度提高到每64位MUL大约5.4个周期(包括所有的添加、移动和循环开销)。我想这大概是Core2的最佳

我正在写数学代码,它需要快速乘法大数。它分解为整数数组与单个整数的乘法。在C++中,这看起来像(在无符号的)上:

当我对此进行基准测试时,我发现在我的Core2 Quad上,每次乘法大约需要6.3个周期

我的问题是:我能不能加快速度?不幸的是,我认为没有办法避免其中一个加法,乘法总是需要RDX:RAX,所以我需要移动数据,不能进行“并行乘法”

有什么想法吗

更新: 经过更多的测试,我已经设法将速度提高到每64位MUL大约5.4个周期(包括所有的添加、移动和循环开销)。我想这大概是Core2的最佳性能,因为Core2没有非常快的MUL指令:它的吞吐量为3,延迟为6(分别为7)个周期。Sandy bridge的吞吐量为1,延迟为3(分别为4)个周期,性能会更好


关于GMP的小得多的数字:我从他们的源代码中得到的,在我看来,这是一个理论数字。但可以肯定的是,这是一个为AMD K9 CPU计算的数字。从我所读到的资料来看,AMD的MUL单元比(旧的)Intel芯片快。

看来您的程序可以从SSE中受益。PMULLD和padd看起来像是相关的说明。我不确定编译器为什么不能从中生成SSE。

我只想指出,循环计数是非常无用的,因为指令将转换为微码,而微码将根据cpu正在执行的其他操作的顺序执行或暂停。如果你有一个快速的例程,你可以这样做,除非你知道你的例程总是在完全隔离的状态下运行,否则尝试去除一个理论上的循环并不是完全有效的。

我曾经写过一个循环,看起来很像这样,对大量数据进行最小的处理,结果是循环受到内存速度的限制

我会尝试预取a[I]和r[I]

如果使用gcc,请在汇编程序中使用函数_内置的_prefetch()或PREFETCHT0指令


当这起作用时,结果可能是戏剧性的。只要循环有一千次左右的迭代,我就会预取a[I+64]和r[I+64]作为起点,看看它对CPU的影响有多大。您可能需要尝试更大的预回迁距离。

呼叫前r是否包含任何重要内容

如果是这样的话,并且你正在积累,那么现在就停止阅读

如果不需要(即总是累加到零),并且假设在远远大于缓存大小的数组上调用此函数,那么我将寻找一种方法来消除从r读取的需要,并将“save result”
MOV
转换为
MOVNT
\u mm\u stream\u ps
在intrinsics中)


这可以显著提高性能。怎样?当前缓存正在从a获取缓存线,从r获取缓存线,并将缓存线写回r。通过调用流媒体存储,您只需从a获取缓存线并直接写入到r:大大减少了总线流量。如果您查看任何现代CRT的memcpy实现,它将切换到使用高于某些缓存大小相关阈值的流式存储(并使用常规移动作为memcpy运行)。

您可能需要查看GMP中的一些组装例程。它们有一个函数可以做到这一点,并且是在汇编中为大多数处理器编写的,包括x64.GMP确实对快速mul_基本情况有很好的支持,并且似乎每个mul需要2.35个周期,非常好。如果我理解正确,它们将交错的两个向量相乘,这似乎可以保持较低的依赖性并改进溢出处理。这适用于32位x 32位乘法。但不适用于64位x 64位乘法。当您只保留最重要的dword时,您真的需要qword乘法吗?我将RAX保存回内存,RDX用作进位(通过R11)并添加到下一个元素中。不幸的是,我需要QWORD MUL.Shucks。SSE似乎无法处理拥挤的四边形,真遗憾。尽管如此,如果你能以某种方式将四次乘法分成两次,一次四次,也许你能获得一些性能。同样,如果GMP是答案,他们有使用PMULUDQ的SSE2代码,但评论表明每个QWORD大约需要4.9个周期。所以我最好试试交错MUL。OP测试了他的代码,显然得到了可重复的结果。他没有计算理论周期,他实际上测量了实践周期。指令转换为微码和重新排序的方式是可预测的,也是众所周知的(见www.agner.org)。此外,优化不需要完全隔离,在后台运行代码的操作系统通常不会减少超过百分之几(如果有的话)。这应该是一个注释,而不是回复。我试过了。结果是,我的Core2四驱车没有任何差别。通过浏览CPU手册和Agner Fog的指南,我得到了这样的想法:今天的处理器有一个很好的预取逻辑,可以很好地检测简单的循环,因此不需要手动预取。这非常有趣。调用函数时,
r
为空,但会慢慢填满。另外,在函数完成后,我希望它将被用于某些事情(因为它是结果:)。我认为MOVNT不会有什么优势,因为我们是按顺序填充
r
。Agner Fog写道:“当且仅当可以预期二级缓存未命中时,无缓存存储数据的方法是有利的”()。我认为二级缓存未命中可以在99%内排除。
void muladd(unsigned* r, const unsigned* a, unsigned len, unsigned b) {
   unsigned __int64 of = 0;  // overflow
   unsigned i = 0;  // loop variable
   while (i < len) {
      of += (unsigned __int64)a[i] * b + r[i];
      r[i] = (unsigned)of;
      of >>= 32;
      ++i;
   }
   r[i] = (unsigned)of;  // save overflow
}
mov   rax, rdi                             ; rdi = b
mul   QWORD PTR [rbx+r10*8-64]             ; rdx:rax = a[i] * b; r10 = i
mov   rsi, QWORD PTR [r14+r10*8-64]        ; r14 = r; rsi = r[i]
add   rax, rsi
adc   rdx, 0
add   rax, r11                             ; r11 = of (low part)
adc   rdx, 0
mov   QWORD PTR [r14+r10*8-64], rax        ; save result
mov   r11, rdx

; this repeats itself 8 times with different offsets