【问题标题】:Does the loop variable type affect efficiency when used to index arrays?用于索引数组时,循环变量类型会影响效率吗?
【发布时间】:2014-07-01 22:23:03
【问题描述】:

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

我用以下程序做了一些实验,它只是用 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,整数

.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,整数

..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 & 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 条指令,就像 add 一样,它可以将寄存器使用减少 1。

【问题讨论】:

  • 索引数组的首选类型是(unsignedsize_t
  • 您使用了什么级别的优化? -O3?

标签: c performance optimization assembly x86


【解决方案1】:

是的,循环变量类型会影响效率。

让我建议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 指令的情况下执行此操作。

【讨论】:

  • @PaulR,这很奇怪,现在我修复了它。 GCC 现在只使用 memset-O3 而不是 -O2。 Clang 使用meset,甚至使用-O2
  • 是的,我只是在godbolt 上玩,看到了同样的东西。
  • 如果你隐藏向量为零的事实,你可以让它与所有版本的 gcc 和 -O3 一起工作:goo.gl/P1JQbG
  • @PaulR,谢谢!我在回答中使用了您的功能。希望你不要介意。
  • @Paul 和 ZB:这是我正在谈论的问答,现在我终于发布了:stackoverflow.com/questions/34377711/…。希望还没有写过类似的东西,因为我花了很多时间来防止它膨胀到两倍长或什么的。 :P
【解决方案2】:

gcc 4.9.2 在编译带有int 循环计数器的版本方面做得很差。 gcc 5.1 and later 在 Godbolt 上进行合理循环:

    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 宏熔断到单个微指令中)。

我不确定为什么 gcc 4.9.2 会产生如此愚蠢的循环。它决定要增加一个指针,但每次都会减去起始地址以与 N 进行比较,而不是计算结束地址并将其用作循环条件。它使用 32 位操作从其指向数组的指针计算 i,这实际上是安全的,因为 gcc 只需要 32b 的结果。如果 gcc 进行了 64 位数学运算,则输入的高位 32b 不会影响结果的低位 32b。

【讨论】:

  • 它可以更加理智。您可以将 rcx 设置为数组的末尾并在 -N 处开始 rax,然后计数到零,然后 jnz 不使用 cmp。
  • @Zboson:是的,如果你要使用索引寻址模式,这是一个很好的方法。但是,在这种情况下,当与 ALU 执行端口上的瓶颈代码共享内核时,它应该只与超线程产生可观察到的差异。好吧,还有代码大小/uop 缓存消耗。
  • 在 OPs 示例中它可能无关紧要,但在其他情况下它可以。我认为 OP 正在寻找一个普遍的答案。不仅仅是他/她的问题。
【解决方案3】:

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

  1. 不要让循环运行超过所需的时间
  2. 如果满足特定条件则中断

用数字填充二维数组,如果你做了上面的2,你的执行复杂度将是

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

循环中的每一行都算作命令数的 +1。

这是从编程角度来看的优化,唯一让它更快的事情是拥有一个更好的处理器,它可以每秒执行更多的命令,但这取决于用户。

编辑:

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

【讨论】:

  • 这如何回答这个问题?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-11-25
  • 2014-12-26
  • 2017-06-27
  • 2023-03-03
相关资源
最近更新 更多