我正在尝试将代码优化到最后一个可能的循环,并且想知道循环类型在用于数组索引时是否会影响性能?
我已经完成了以下程序的一些实验,该程序只用0填充数组:
int main(int argc, char **argv)
{
typedef int CounterType;
typedef int64_t CounterType;
CounterType N = atoi(argv[1]);
uint8_t volatile dummy[N + 16];
__m128i v = _mm_set1_epi8(0);
for (int j = 0; j < 1000000; ++j)
{
#pragma nounroll
for (CounterType i = 0; i <= N; i+= CounterType(16))
{
_mm_storeu_si128((__m128i *)&dummy[i], v);
}
}
return 0;
}
通过使用不同的循环计数器类型(CounterType)和不同的编译器, 我已经使用硬件性能计数器(“perf stat a.out 32768”)记录了内循环和性能的汇编代码。我在Xeon 5670上运行。
GCC4.9,int
.L3
movups %xmm0, (%rax)
addq $16, %rax
movl %eax, %edx
subl %esi, %edx
cmpl %ecx, %edx
jle .L3
4,127,525,521 cycles # 2.934 GHz
12,304,723,292 instructions # 2.98 insns per cycle
GCC4.9,int64
.L7
movups %xmm0, (%rcx,%rax)
addq $16, %rax
cmpq %rax, %rdx
jge .L7
4,123,315,191 cycles # 2.934 GHz
8,206,745,195 instructions # 1.99 insns per cycle
ICC11,int64
..B1.6:
movdqu %xmm0, (%rdx,%rdi)
addq $16, %rdx
incq %rcx
cmpq %rbx, %rcx
jb ..B1.6 # Prob 82% #24.5
2,069,719,166 cycles # 2.934 GHz
5,130,061,268 instructions
(由于微操作融合更快?)
ICC11,int
..B1.6: # Preds ..B1.4 ..B1.6
movdqu %xmm0, (%rdx,%rbx) #29.38
addq $16, %rdx #24.37
cmpq %rsi, %rdx #24.34
jle ..B1.6 # Prob 82% #24.34
4,136,109,529 cycles # 2.934 GHz
8,206,897,268 instructions
ICC13,int&amp;的int64
movdqu %xmm0, (%rdi,%rax) #29.38
addq $16, %rdi #24.37
cmpq %rsi, %rdi #24.34
jle ..B1.7
4,123,963,321 cycles # 2.934 GHz
8,206,083,789 instructions # 1.99 insns per cycle
数据似乎表明int64更快。也许这是因为它匹配指针大小,因此避免任何转换。但我不相信这个结论。另一种可能性是编译器在某些情况下决定在存储之前进行循环比较,以便以1个额外指令为代价实现更多并行性(由于X86 2操作数指令具有破坏性)。但这是偶然的,并不是由循环变量类型引起的。
有人可以解释这个谜(最好是有关编译器转换的知识)吗?
在CUDA C最佳实践指南中还有一项声明,即签名循环计数器比无符号生成代码要简单。但这似乎并不重要,因为内部循环中没有乘法用于地址计算,因为该表达式变为归纳变量。但显然在CUDA中,它更喜欢使用乘法加法来计算地址,因为MADD就像加法一样是1条指令,它可以将寄存器的使用减少1。
答案 0 :(得分:2)
是的循环变量类型会影响效率。
我建议an even better solution with GCC。
void distance(uint8_t* dummy, size_t n, const __m128 v0)
{
intptr_t i;
for(i = -n; i < 0; i += 4) {
_mm_store_ps(&((float*)dummy)[i+n], v0);
}
}
使用GCC 4.9.2和GCC 5.3,这将产生这个主循环
.L5:
vmovaps %xmm0, (%rdi,%rax)
addq $16, %rax
js .L5
然而, Clang 3.6仍会生成cmp
.LBB0_2: # =>This Inner Loop Header:
vmovaps %xmm0, 8(%rdi,%rax)
addq $4, %rax
cmpq $-4, %rax
jl .LBB0_2
和Clang 3.7展开四次并使用cmp
。
ICC 13展开两次并使用cmp
,因此只有GCC设法在没有不必要的cmp
指令的情况下执行此操作。
答案 1 :(得分:1)
gcc 4.9.2使用int
循环计数器编译版本的工作非常糟糕。关于godbolt的gcc 5.1 and later做出理智的循环:
call strtol
mov edx, eax
...
xor eax, eax
.L7:
movups XMMWORD PTR [rcx+rax], xmm0
add rax, 16
cmp edx, eax
jge .L7 ; while(N >= (idx+=16))
这可以在Intel CPU上每个周期运行一次(除了L1缓存未命中瓶颈),即使商店没有微熔丝(因为cmp / jge宏融合成单个uop)。
我不确定为什么gcc 4.9.2会造成如此愚蠢的循环。它决定它想要递增指针,但是它每次都减去起始地址以与N进行比较,而不是计算结束地址并将其用作循环条件。它使用32位操作从其指向数组的指针计算i
,其中 实际上是安全的,因为gcc只需要32b的结果。如果gcc已完成64位数学运算,输入的高32b不会影响结果的低32b。
答案 2 :(得分:-1)
据我所知,循环类型不会影响性能和执行速度,对于优化,只有重要的事情是:
如果使用数字填充2D数组,如果执行上述2,则执行复杂度为
(元素数量)*(循环内的命令数量)
循环中的每一行都计为+1到命令数。
这是编程视图的优化,只有使其更快的其他事情是拥有一个更好的处理器,可以每秒执行更多的命令,但这取决于用户。
修改强>
请注意,在某些情况下,使用指向数组的指针并在单个循环中填充元素而不是具有2个循环会更快。 C允许相同算法的很多变化。