我正在尝试优化一个循环中的指令块,称为数千次,这是我算法中的瓶颈。
该代码块计算N个矩阵3x3(iA阵列)与N个向量3(iV阵列)的乘法,并将N个结果存储在oV阵列中。 (N不固定,通常在3000到15000之间)
每行矩阵和向量都是128位对齐(4个浮点数)以利用SSE优化(忽略第4个浮点值)。
C ++代码:
__m128* ip = (__m128*)iV;
__m128* op = (__m128*)oV;
__m128* A = (__m128*)iA;
__m128 res1, res2, res3;
int i;
for (i=0; i<N; i++)
{
res1 = _mm_dp_ps(*A++, *ip, 0x71);
res2 = _mm_dp_ps(*A++, *ip, 0x72);
res3 = _mm_dp_ps(*A++, *ip++, 0x74);
*op++ = _mm_or_ps(res1, _mm_or_ps(res2, res3));
}
编译器生成以下指令:
000007FEE7DD4FE0 movaps xmm2,xmmword ptr [rsi] //move "ip" in register
000007FEE7DD4FE3 movaps xmm1,xmmword ptr [rdi+10h] //move second line of A in register
000007FEE7DD4FE7 movaps xmm0,xmmword ptr [rdi+20h] //move third line of A in register
000007FEE7DD4FEB inc r11d //i++
000007FEE7DD4FEE add rbp,10h //op++
000007FEE7DD4FF2 add rsi,10h //ip++
000007FEE7DD4FF6 dpps xmm0,xmm2,74h //dot product of 3rd line of A against ip
000007FEE7DD4FFC dpps xmm1,xmm2,72h //dot product of 2nd line of A against ip
000007FEE7DD5002 orps xmm0,xmm1 //"merge" of the result of the two dot products
000007FEE7DD5005 movaps xmm3,xmmword ptr [rdi] //move first line of A in register
000007FEE7DD5008 add rdi,30h //A+=3
000007FEE7DD500C dpps xmm3,xmm2,71h //dot product of 1st line of A against ip
000007FEE7DD5012 orps xmm0,xmm3 //"merge" of the result
000007FEE7DD5015 movaps xmmword ptr [rbp-10h],xmm0 //move result in memory (op)
000007FEE7DD5019 cmp r11d,dword ptr [rbx+28h] //compare i
000007FEE7DD501D jl MyFunction+370h (7FEE7DD4FE0h) //loop
我对低级优化并不是很熟悉,所以问题是:如果我自己编写汇编代码,你会看到一些可能的优化吗?
例如,如果我改变它会运行得更快:
add rbp,10h
movaps xmmword ptr [rbp-10h],xmm0
通过
movaps xmmword ptr [rbp],xmm0
add rbp,10h
我还读到ADD指令比INC ...
更快答案 0 :(得分:3)
使用偏移计算间接地址,例如rbp-10
非常便宜,因为在“有效地址计算”单元中有这些计算的特殊硬件[我认为它有不同的名称,但可以'想到谷歌找到它的名字或取得任何成功]。
然而,add rbp,10h
和[rbp-10h]
之间存在依赖关系,这可能会导致问题 - 但我在这种特殊情况下对此表示怀疑。在您的情况下,rbp-10
与使用它之间有很长的距离,所以这不是问题。编译器可能把它放得那么远,因为它在那时是“空闲的”,因为处理器将等待数据从外部进入先前已读取的xmm寄存器。换句话说,我们可以在循环开始时读取xmm0
,xmm1
和xmm2
以及使用{{1}的dpps
指令之间的任何工作},xmm0
和xmm1
将是有益的,因为处理器将等待该数据“到达”,然后才能计算xmm2
结果。
答案 1 :(得分:2)
我做了很多x86程序集优化,我可以告诉你这是一次很棒的学习经历。它教会了我很多关于编译器如何工作的知识,而我学到的最重要的事情是编译器通常都非常擅长于他们的工作。我知道这是一个轻率的评论,但这是真的......
我还了解到,您所做的优化可以在一个处理器系列上产生积极的结果,在另一个处理器系列上产生负面结果。像流水线,分支预测和处理器缓存这样的事情起着巨大的作用......因此,除非您针对非常具体的硬件配置,否则请注意有关您所做改进的假设。
关于重新排序添加以删除rbp-10h
偏移量的具体问题...它看起来像是一个明显的改进,您必须通过阅读说明手册进行验证,但我猜{ {1}}内存偏移量在该指令中免费提供。移动-10h
可能会抛出流水线指令,实际上会导致时钟周期丢失。你必须进行实验。
答案 2 :(得分:1)
您可以对上述代码进行一些改进。通常,在更改后使用值会导致处理器在等待结果时停止。所以这些线条会受到惩罚: -
add rbp,10h
movaps xmmword ptr [rbp-10h],xmm0
但是在这两行之上的代码片段相距很远,所以这不是一个真正的问题。正如其他人已经说过的那样,rbp-10h
是“免费的”,因为地址计算硬件会处理它。
您可以将movaps xmm3,xmmword ptr [rdi]
向上移动一行,也可以重新排列其他几行。
值得吗?
否强>
你很幸运能看到任何真正的性能提升,因为你的算法是
<blink> memory bandwidth limited </blink>*
这意味着从RAM读取数据到CPU的时间大于CPU执行处理所花费的时间。在最坏的情况下,读取内存地址可能涉及页面错误和磁盘读取。 prefetch
指令也无济于事,它被称为“流式SIMD扩展”,因为它经过优化可以将数据流式传输到CPU中(存储器接口可以处理四个独立的流IIRC)。
如果你在一小组数据(也许是FFT)上进行大量计算,那么你可以从手工制作汇编程序中获得很多收益。但是你的算法非常简单,因此CPU在等待数据到达的大部分时间都处于闲置状态。如果您知道RAM的速度,您可以计算算法的最大吞吐量,并使用它来与当前实现的目标进行比较(尽管如此,您永远不会达到最大理论吞吐量)。
您可以采取一些措施来最大程度地减少内存停滞,这是一个更高级别的更改,而不是摆弄单个指令(通常,优化算法会获得更好的结果)。最简单的是对输入数据进行双重缓冲。将寄存器组分为两组(可以在此处执行,因为您只使用四个SIMD寄存器): -
load set 1
mainloop:
load set 2
do processing on set 1
save set 1 result
load set 1
do processing on set 2
save set 2 result
goto mainloop
希望这能给你一些想法。即使它没有快得多,它仍然是一个有趣的练习,你可以从中学到很多东西。