SSE微优化指令顺序
SSE micro-optimization instruction order
我注意到有时MSVC 2010根本不重新排序SSE指令。我认为我不必关心循环中的指令顺序,因为编译器可以最好地处理它,但事实似乎并非如此。
我该怎么想呢?什么决定了最好的指令顺序?我知道有些指令比其他指令有更高的延迟,有些指令可以在cpu级别上并行/异步运行。在上下文中哪些指标是相关的?我在哪里可以找到它们?
我知道我可以通过分析来避免这个问题,但是这样的分析器是昂贵的(VTune XE)和我想知道它背后的理论,而不仅仅是经验结果。
我还应该关心软件预取(_mm_prefetch
)还是我可以假设cpu会比我做得更好?
假设我有以下函数。我应该穿插一些说明吗?我是否应该在流之前进行存储,按顺序进行所有加载,然后再进行计算等等?我是否需要考虑USWC与非USWC,以及临时与非临时?
auto cur128 = reinterpret_cast<__m128i*>(cur);
auto prev128 = reinterpret_cast<const __m128i*>(prev);
auto dest128 = reinterpret_cast<__m128i*>(dest;
auto end = cur128 + count/16;
while(cur128 != end)
{
auto xmm0 = _mm_add_epi8(_mm_load_si128(cur128+0), _mm_load_si128(prev128+0));
auto xmm1 = _mm_add_epi8(_mm_load_si128(cur128+1), _mm_load_si128(prev128+1));
auto xmm2 = _mm_add_epi8(_mm_load_si128(cur128+2), _mm_load_si128(prev128+2));
auto xmm3 = _mm_add_epi8(_mm_load_si128(cur128+3), _mm_load_si128(prev128+3));
// dest128 is USWC memory
_mm_stream_si128(dest128+0, xmm0);
_mm_stream_si128(dest128+1, xmm1);
_mm_stream_si128(dest128+2, xmm2);;
_mm_stream_si128(dest128+3, xmm3);
// cur128 is temporal, and will be used next time, which is why I choose store over stream
_mm_store_si128 (cur128+0, xmm0);
_mm_store_si128 (cur128+1, xmm1);
_mm_store_si128 (cur128+2, xmm2);
_mm_store_si128 (cur128+3, xmm3);
cur128 += 4;
dest128 += 4;
prev128 += 4;
}
std::swap(cur, prev);
我同意每个人都认为测试和调整是最好的方法。但是有一些技巧可以帮助它。
首先,MSVC 对重新排序SSE指令。你的例子可能太简单或者已经是最优的了。
一般来说,如果你有足够的寄存器这样做,完全交错往往会得到最好的结果。更进一步,展开循环以使用所有寄存器,但不要过多溢出。在您的示例中,循环完全由内存访问绑定,因此没有太多的空间可以做得更好。
在大多数情况下,没有必要使指令的顺序完美以达到最佳性能。只要它"足够接近",编译器或硬件的乱序执行都会为你修复它。
我用来确定代码是否最优的方法是关键路径和瓶颈分析。写完循环后,我查找哪些指令使用了哪些资源。使用这些信息,我可以计算性能的上限,然后将其与实际结果进行比较,看看我离最佳值有多近/多远。
例如,假设我有一个有100个加法和50个乘法的循环。在Intel和AMD (pre-推土机)上,每个内核每个周期可以维持一个SSE/AVX加法和一个SSE/AVX乘法。因为我的循环有100个添加,所以我知道我不能做得比100个循环更好。是的,乘法器有一半的时间是空闲的,但是加法器才是瓶颈。现在我对循环计时每次迭代得到105个循环。这意味着我已经非常接近最优值了,不会有更多的增益了。但如果我得到250个循环,那就意味着循环出了问题,值得再修改一下。
关键路径分析遵循相同的思想。查找所有指令的延迟,并找到循环的关键路径的周期时间。如果你的实际表现非常接近它,你已经是最佳的了。
Agner Fog对当前处理器的内部细节有一个很好的参考:http://www.agner.org/optimize/microarchitecture.pdf
我刚刚使用VS2010 32位编译器构建了这个,我得到以下内容:
void F (void *cur, const void *prev, void *dest, int count)
{
00901000 push ebp
00901001 mov ebp,esp
00901003 and esp,0FFFFFFF8h
__m128i *cur128 = reinterpret_cast<__m128i*>(cur);
00901006 mov eax,220h
0090100B jmp F+10h (901010h)
0090100D lea ecx,[ecx]
const __m128i *prev128 = reinterpret_cast<const __m128i*>(prev);
__m128i *dest128 = reinterpret_cast<__m128i*>(dest);
__m128i *end = cur128 + count/16;
while(cur128 != end)
{
auto xmm0 = _mm_add_epi8(_mm_load_si128(cur128+0), _mm_load_si128(prev128+0));
00901010 movdqa xmm0,xmmword ptr [eax-220h]
auto xmm1 = _mm_add_epi8(_mm_load_si128(cur128+1), _mm_load_si128(prev128+1));
00901018 movdqa xmm1,xmmword ptr [eax-210h]
auto xmm2 = _mm_add_epi8(_mm_load_si128(cur128+2), _mm_load_si128(prev128+2));
00901020 movdqa xmm2,xmmword ptr [eax-200h]
auto xmm3 = _mm_add_epi8(_mm_load_si128(cur128+3), _mm_load_si128(prev128+3));
00901028 movdqa xmm3,xmmword ptr [eax-1F0h]
00901030 paddb xmm0,xmmword ptr [eax-120h]
00901038 paddb xmm1,xmmword ptr [eax-110h]
00901040 paddb xmm2,xmmword ptr [eax-100h]
00901048 paddb xmm3,xmmword ptr [eax-0F0h]
// dest128 is USWC memory
_mm_stream_si128(dest128+0, xmm0);
00901050 movntdq xmmword ptr [eax-20h],xmm0
_mm_stream_si128(dest128+1, xmm1);
00901055 movntdq xmmword ptr [eax-10h],xmm1
_mm_stream_si128(dest128+2, xmm2);;
0090105A movntdq xmmword ptr [eax],xmm2
_mm_stream_si128(dest128+3, xmm3);
0090105E movntdq xmmword ptr [eax+10h],xmm3
// cur128 is temporal, and will be used next time, which is why I choose store over stream
_mm_store_si128 (cur128+0, xmm0);
00901063 movdqa xmmword ptr [eax-220h],xmm0
_mm_store_si128 (cur128+1, xmm1);
0090106B movdqa xmmword ptr [eax-210h],xmm1
_mm_store_si128 (cur128+2, xmm2);
00901073 movdqa xmmword ptr [eax-200h],xmm2
_mm_store_si128 (cur128+3, xmm3);
0090107B movdqa xmmword ptr [eax-1F0h],xmm3
cur128 += 4;
00901083 add eax,40h
00901086 lea ecx,[eax-220h]
0090108C cmp ecx,10h
0090108F jne F+10h (901010h)
dest128 += 4;
prev128 += 4;
}
}
表示编译器正在重新排序指令,遵循"在写入寄存器后不要立即使用寄存器"的一般规则。它还将两次加载和一次添加转换为一次加载和一次内存添加。您没有理由不能自己编写这样的代码,并使用所有SIMD寄存器,而不是当前使用的四个寄存器。您可能希望将加载的字节总数与缓存行的大小相匹配。这将给硬件预取一个机会在你需要它之前填充下一个缓存行。
同样,预取,特别是在顺序读取内存的代码中,通常是不必要的。MMU最多可以一次预取4个流
你可能会发现英特尔架构优化参考手册的第5章到第7章非常有趣,它详细说明了英特尔认为你应该如何编写最佳的SSE代码,并且它详细说明了你正在询问的许多问题。
我还想推荐英特尔®架构代码分析器:
https://software.intel.com/en-us/articles/intel-architecture-code-analyzer它是一个静态代码分析器,可以帮助找出/优化关键路径、延迟和吞吐量。它适用于Windows、Linux和MacOs(我只在Linux上试用过)。文档中有一个中等简单的例子来说明如何使用它(即,如何通过重新排序指令来避免延迟)。
- 使用C++库在Android项目中修改gradle中的cmake参数,用于插入指令的测试
- CMake-按正确顺序将项目与C运行时对象文件链接
- 函数调用中参数的顺序重要吗
- 为什么不;名字在地图上是按顺序排列的吗
- 无法编译 rtmidi 测试 cmidiin.cpp 文件, 非法指令
- 将Integer转换为4字节的unsined字符矢量(按大端字节顺序)
- 数到第n个楼梯的路(顺序无关紧要)
- C++:对不存在的命名空间使用命名空间指令
- 优先顺序:智能指针和类析构函数
- 在循环中按顺序遍历成员变量
- 独立读取-修改-写入顺序
- QML按钮点击功能执行顺序
- C++中数据类型修饰符的顺序
- 当比特(而不是字节)的顺序至关重要时的持久性
- RDTSCP 和指令顺序
- C :使用声明和指令的顺序会影响选择
- 为什么sleep in函数忽略了程序中的几个顺序指令
- #include 指令和"using"语句的顺序在C++头文件的开头是否重要?
- 为什么改变这些指令的顺序会显著影响性能
- SSE微优化指令顺序