SSE 内联函数和循环展开
SSE Intrinsics and loop unrolling
我正在尝试优化一些循环,我已经做到了,但我想知道我是否只做了部分正确的事情。例如,假设我有这个循环:
for(i=0;i<n;i++){
b[i] = a[i]*2;
}
将其展开 3 倍,产生这个:
int unroll = (n/4)*4;
for(i=0;i<unroll;i+=4)
{
b[i] = a[i]*2;
b[i+1] = a[i+1]*2;
b[i+2] = a[i+2]*2;
b[i+3] = a[i+3]*2;
}
for(;i<n;i++)
{
b[i] = a[i]*2;
}
现在是 SSE 翻译等效项:
__m128 ai_v = _mm_loadu_ps(&a[i]);
__m128 two_v = _mm_set1_ps(2);
__m128 ai2_v = _mm_mul_ps(ai_v, two_v);
_mm_storeu_ps(&b[i], ai2_v);
还是:
__m128 ai_v = _mm_loadu_ps(&a[i]);
__m128 two_v = _mm_set1_ps(2);
__m128 ai2_v = _mm_mul_ps(ai_v, two_v);
_mm_storeu_ps(&b[i], ai2_v);
__m128 ai1_v = _mm_loadu_ps(&a[i+1]);
__m128 two1_v = _mm_set1_ps(2);
__m128 ai_1_2_v = _mm_mul_ps(ai1_v, two1_v);
_mm_storeu_ps(&b[i+1], ai_1_2_v);
__m128 ai2_v = _mm_loadu_ps(&a[i+2]);
__m128 two2_v = _mm_set1_ps(2);
__m128 ai_2_2_v = _mm_mul_ps(ai2_v, two2_v);
_mm_storeu_ps(&b[i+2], ai_2_2_v);
__m128 ai3_v = _mm_loadu_ps(&a[i+3]);
__m128 two3_v = _mm_set1_ps(2);
__m128 ai_3_2_v = _mm_mul_ps(ai3_v, two3_v);
_mm_storeu_ps(&b[i+3], ai_3_2_v);
我对代码部分有点困惑:
for(;i<n;i++)
{
b[i] = a[i]*2;
}
这是做什么的?是否只是为了做额外的部分,例如,如果循环不能除以您选择展开它的因子?谢谢。
答案是第一个块:
__m128 ai_v = _mm_loadu_ps(&a[i]);
__m128 two_v = _mm_set1_ps(2);
__m128 ai2_v = _mm_mul_ps(ai_v,two_v);
_mm_storeu_ps(&b[i],ai2_v);
它一次需要四个变量。
这是完整的程序,其中注释掉了等效的代码部分:
#include <iostream>
int main()
{
int i{0};
float a[10] ={1,2,3,4,5,6,7,8,9,10};
float b[10] ={0,0,0,0,0,0,0,0,0,0};
int n = 10;
int unroll = (n/4)*4;
for (i=0; i<unroll; i+=4) {
//b[i] = a[i]*2;
//b[i+1] = a[i+1]*2;
//b[i+2] = a[i+2]*2;
//b[i+3] = a[i+3]*2;
__m128 ai_v = _mm_loadu_ps(&a[i]);
__m128 two_v = _mm_set1_ps(2);
__m128 ai2_v = _mm_mul_ps(ai_v,two_v);
_mm_storeu_ps(&b[i],ai2_v);
}
for (; i<n; i++) {
b[i] = a[i]*2;
}
for (auto i : a) { std::cout << i << "t"; }
std::cout << "n";
for (auto i : b) { std::cout << i << "t"; }
std::cout << "n";
return 0;
}
至于效率;似乎我的系统上的程序集生成movups
指令,而手动滚动代码可以使用movaps
应该更快。
我使用以下程序做了一些基准测试:
#include <iostream>
//#define NO_UNROLL
//#define UNROLL
//#define SSE_UNROLL
#define SSE_UNROLL_ALIGNED
int main()
{
const size_t array_size = 100003;
#ifdef SSE_UNROLL_ALIGNED
__declspec(align(16)) int i{0};
__declspec(align(16)) float a[array_size] ={1,2,3,4,5,6,7,8,9,10};
__declspec(align(16)) float b[array_size] ={0,0,0,0,0,0,0,0,0,0};
#endif
#ifndef SSE_UNROLL_ALIGNED
int i{0};
float a[array_size] ={1,2,3,4,5,6,7,8,9,10};
float b[array_size] ={0,0,0,0,0,0,0,0,0,0};
#endif
int n = array_size;
int unroll = (n/4)*4;
for (size_t j{0}; j < 100000; ++j) {
#ifdef NO_UNROLL
for (i=0; i<n; i++) {
b[i] = a[i]*2;
}
#endif
#ifdef UNROLL
for (i=0; i<unroll; i+=4) {
b[i] = a[i]*2;
b[i+1] = a[i+1]*2;
b[i+2] = a[i+2]*2;
b[i+3] = a[i+3]*2;
}
#endif
#ifdef SSE_UNROLL
for (i=0; i<unroll; i+=4) {
__m128 ai_v = _mm_loadu_ps(&a[i]);
__m128 two_v = _mm_set1_ps(2);
__m128 ai2_v = _mm_mul_ps(ai_v,two_v);
_mm_storeu_ps(&b[i],ai2_v);
}
#endif
#ifdef SSE_UNROLL_ALIGNED
for (i=0; i<unroll; i+=4) {
__m128 ai_v = _mm_load_ps(&a[i]);
__m128 two_v = _mm_set1_ps(2);
__m128 ai2_v = _mm_mul_ps(ai_v,two_v);
_mm_store_ps(&b[i],ai2_v);
}
#endif
#ifndef NO_UNROLL
for (; i<n; i++) {
b[i] = a[i]*2;
}
#endif
}
//for (auto i : a) { std::cout << i << "t"; }
//std::cout << "n";
//for (auto i : b) { std::cout << i << "t"; }
//std::cout << "n";
return 0;
}
我得到了以下结果 (x86):
-
NO_UNROLL
:0.994秒,编译器未选择SSE -
UNROLL
: 3.511 秒, 使用movups
-
SSE_UNROLL
: 3.315 秒, 使用movups
-
SSE_UNROLL_ALIGNED
:3.276秒,使用movaps
因此,很明显,在这种情况下展开循环无济于事。即使确保我们使用更高效的movaps
也没有多大帮助。
但是在编译到 64 位 (x64) 时,我得到了一个更奇怪的结果:
-
NO_UNROLL
:1.138秒,编译器未选择SSE -
UNROLL
:1.409秒,编译器未选择SSE -
SSE_UNROLL
:1.420秒,编译器仍然没有选择SSE! -
SSE_UNROLL_ALIGNED
:1.476秒,编译器仍然没有选择SSE!
似乎 MSVC 看穿了提案并生成了更好的组装,尽管仍然比我们根本没有尝试任何手动优化要慢。
像往常一样,展开循环并尝试手动匹配 SSE 指令效率不高。编译器可以比你做得更好。例如,提供的示例会自动编译到启用 SSE 的 ASM 中:
foo:
.LFB0:
.cfi_startproc
testl %edi, %edi
jle .L7
movl %edi, %esi
shrl $2, %esi
cmpl $3, %edi
leal 0(,%rsi,4), %eax
jbe .L8
testl %eax, %eax
je .L8
vmovdqa .LC0(%rip), %xmm1
xorl %edx, %edx
xorl %ecx, %ecx
.p2align 4,,10
.p2align 3
.L6:
addl $1, %ecx
vpmulld a(%rdx), %xmm1, %xmm0
vmovdqa %xmm0, b(%rdx)
addq $16, %rdx
cmpl %esi, %ecx
jb .L6
cmpl %eax, %edi
je .L7
.p2align 4,,10
.p2align 3
.L9:
movslq %eax, %rdx
addl $1, %eax
movl a(,%rdx,4), %ecx
addl %ecx, %ecx
cmpl %eax, %edi
movl %ecx, b(,%rdx,4)
jg .L9
.L7:
rep
ret
.L8:
xorl %eax, %eax
jmp .L9
.cfi_endproc
循环也可以展开,它只会使代码更长,我不想在这里粘贴。你可以相信我 - 编译器确实会展开循环。
结论
手动展开对你没有好处。
相关文章:
- 循环展开 - G++ 与 Clang++
- 这个返回元素位置的基于循环的函数有什么问题?
- 优化在网格图中查找哈密尔循环的函数?
- 我必须更改我的数字最后一个数字和第一个数字,但不要使用仅带有整数或循环的函数.例如从 12345 到 52341
- 通过循环展开和阻塞进行优化
- 为什么Visual Studio Compiler不在我的Mersenne-Twister实现中循环展开?
- 在循环和函数中使用用户输入
- 不受控制的循环和函数 C++跳过
- 循环展开和元编程(TMP)
- 是否可以创建一个跳过循环中函数的计时器
- 为什么在 XCode 中默认循环展开
- 为什么我的 for 循环在函数中不递增
- 通过for循环调用函数
- 内存分配,用于在C 11中循环中函数的返回值:如何优化
- 是否有任何预处理器指令控制循环展开
- C++模板元编程成员函数循环展开
- C++循环展开性能差异(欧拉项目)
- SSE 内联函数和循环展开
- 具有嵌套循环和函数调用的 OpenMP
- 从 Android NDK 共享对象中删除异常/展开函数