循环变量类型在用于索引数组时会影响效率吗?

时间:2014-05-14 02:55:35

标签: c performance optimization assembly x86

我正在尝试将代码优化到最后一个可能的循环,并且想知道循环类型在用于数组索引时是否会影响性能?

我已经完成了以下程序的一些实验,该程序只用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。

3 个答案:

答案 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)

据我所知,循环类型不会影响性能和执行速度,对于优化,只有重要的事情是:

  1. 不要让循环运行得更多,然后才需要
  2. 如果满足特定条件则中断
  3. 如果使用数字填充2D数组,如果执行上述2,则执行复杂度为

    (元素数量)*(循环内的命令数量)

    循环中的每一行都计为+1到命令数。

    这是编程视图的优化,只有使其更快的其他事情是拥有一个更好的处理器,可以每秒执行更多的命令,但这取决于用户。

    修改

    请注意,在某些情况下,使用指向数组的指针并在单个循环中填充元素而不是具有2个循环会更快。 C允许相同算法的很多变化。