【问题标题】:Can Compiler Optimize Loop with Variable Length?编译器可以优化可变长度的循环吗?
【发布时间】:2017-02-05 01:29:35
【问题描述】:

如果循环的最后一个索引(以下示例中的ab)在编译时未知,编译器能否优化循环?

未优化:

int* arr = new int[a*b];
for (i = 0; i < a; ++i){
    for(j = 0; j < b; ++j){
        arr[i*b+j] *= 8;
    }
}

//delete arr after done.

更优化:(假设 a 和 b 很大...)

int c = a*b;
int* arr = new int[c];
for (i = 0; i < c; ++i){
        arr[c] *= 8;
}

//delete arr after done.

【问题讨论】:

  • 第一个例子不正确。
  • 你的意思是j*a+i
  • 理论上是可以的,前提是第一个示例是固定的,并且编译器可以确定没有其他变量可以修改。实际上,给定的编译器可能会也可能不会这样做。没有一个答案适用于每个编译器。
  • 你可能想要初始化数组而不是读取未初始化的值。
  • 您的程序有未定义的行为。在您的第一个示例中,假设 ab 分别为 1 和 7。结果数组将被分配以容纳 7 个整数。对您的数组的第一个分配将是arr[0*1+7] = 8,相当于arr[8] = 0。您刚刚写到缓冲区的末尾。我继续编辑你的问题,因为我假设你的意思是arr[ i * a + j] = 8

标签: c++ performance loops optimization compiler-optimization


【解决方案1】:

如果您将数组视为线性空间,gcc(可能还有其他)将优化,即使在编译时不知道范围。

这段代码:

void by8(int* arr, int a, int b)
{
  auto extent = a * b;
  for (int i = 0; i < extent; ++i)
  {
    arr[i] *= 8;
  }
}

编译成这个(注意循环的内部是如何向量化的)

by8(int*, int, int):
        imull   %esi, %edx
        testl   %edx, %edx
        jle     .L23
        movq    %rdi, %rax
        andl    $31, %eax
        shrq    $2, %rax
        negq    %rax
        andl    $7, %eax
        cmpl    %edx, %eax
        cmova   %edx, %eax
        cmpl    $8, %edx
        jg      .L26
        movl    %edx, %eax
.L3:
        sall    $3, (%rdi)
        cmpl    $1, %eax
        je      .L15
        sall    $3, 4(%rdi)
        cmpl    $2, %eax
        je      .L16
        sall    $3, 8(%rdi)
        cmpl    $3, %eax
        je      .L17
        sall    $3, 12(%rdi)
        cmpl    $4, %eax
        je      .L18
        sall    $3, 16(%rdi)
        cmpl    $5, %eax
        je      .L19
        sall    $3, 20(%rdi)
        cmpl    $6, %eax
        je      .L20
        sall    $3, 24(%rdi)
        cmpl    $7, %eax
        je      .L21
        sall    $3, 28(%rdi)
        movl    $8, %ecx
.L5:
        cmpl    %eax, %edx
        je      .L27
.L4:
        leal    -1(%rdx), %r8d
        movl    %edx, %r9d
        movl    %eax, %r10d
        subl    %eax, %r9d
        subl    %eax, %r8d
        leal    -8(%r9), %esi
        shrl    $3, %esi
        addl    $1, %esi
        leal    0(,%rsi,8), %r11d
        cmpl    $6, %r8d
        jbe     .L7
        leaq    (%rdi,%r10,4), %r10
        xorl    %eax, %eax
        xorl    %r8d, %r8d
.L9:
        vmovdqa (%r10,%rax), %ymm0
        addl    $1, %r8d
        vpslld  $3, %ymm0, %ymm0
        vmovdqa %ymm0, (%r10,%rax)
        addq    $32, %rax
        cmpl    %r8d, %esi
        ja      .L9
        addl    %r11d, %ecx
        cmpl    %r11d, %r9d
        je      .L22
        vzeroupper
.L7:
        movslq  %ecx, %rax
        sall    $3, (%rdi,%rax,4)
        leal    1(%rcx), %eax
        cmpl    %eax, %edx
        jle     .L23
        cltq
        sall    $3, (%rdi,%rax,4)
        leal    2(%rcx), %eax
        cmpl    %eax, %edx
        jle     .L23
        cltq
        sall    $3, (%rdi,%rax,4)
        leal    3(%rcx), %eax
        cmpl    %eax, %edx
        jle     .L23
        cltq
        sall    $3, (%rdi,%rax,4)
        leal    4(%rcx), %eax
        cmpl    %eax, %edx
        jle     .L23
        cltq
        sall    $3, (%rdi,%rax,4)
        leal    5(%rcx), %eax
        cmpl    %eax, %edx
        jle     .L23
        cltq
        addl    $6, %ecx
        sall    $3, (%rdi,%rax,4)
        cmpl    %ecx, %edx
        jle     .L28
        movslq  %ecx, %rcx
        sall    $3, (%rdi,%rcx,4)
        ret
.L22:
        vzeroupper
.L23:
        ret
.L27:
        ret
.L26:
        testl   %eax, %eax
        jne     .L3
        xorl    %ecx, %ecx
        jmp     .L4
.L28:
        ret
.L21:
        movl    $7, %ecx
        jmp     .L5
.L15:
        movl    $1, %ecx
        jmp     .L5
.L16:
        movl    $2, %ecx
        jmp     .L5
.L17:
        movl    $3, %ecx
        jmp     .L5
.L18:
        movl    $4, %ecx
        jmp     .L5
.L19:
        movl    $5, %ecx
        jmp     .L5
.L20:
        movl    $6, %ecx
        jmp     .L5

编译器:gcc 5.4,带有命令行选项:-std=c++14 -O3 -march=native

【讨论】:

  • 能否将“未优化的版本”优化为“更优化”的版本?
  • @rxu 我猜这取决于编译器
  • 可能编译器无法融合循环(从未优化到更优化的版本)。这是令人惊讶的。或者我做错了什么。 stackoverflow.com/questions/39758300/…
【解决方案2】:

是的,它可能可以,因为大小是恒定的并且不会在循环中改变,就像这里发生的那样。请阅读Optimize "for" loop 了解更多信息。


仅供参考,在你的第一个例子中,这个:

arr[j*a+b] *= 8;

应该是这样的:

arr[j*a+i] *= 8;

【讨论】:

  • ...或者更确切地说是arr[i*b+j] *= 8l,为了内存访问局部性。
  • 当然@CiaPan,但让我们先把它做好,然后再优化! :)
  • 以下链接中的这三个循环如何不提供相同的二进制文件。 godbolt.org/g/W4KSlC
  • 我在链接的问题中看到一个循环,在接受的答案中看到一个循环@rxu..:/
  • 如果 extent 不是字面量,编译器有时无法融合循环。我对此提出了一个新问题。 (stackoverflow.com/questions/39758300/…)
【解决方案3】:

现代编译器绝对可以改变两个数组的顺序,防止不必要的缓存未命中,来自:

for (i = 0; i < a; ++i){
    for(j = 0; j < b; ++j){
        arr[j*a+i] *= 8;
    }
}

到这里:

for(j = 0; j < b; ++j){
    for (i = 0; i < a; ++i){
        arr[j*a+i] *= 8;
    }
}

经过此优化后,这两个示例(与您的手动优化相比)的性能应该不会有明显差异。

【讨论】:

  • 但它可能并不总是将两个循环融合为一个循环。
【解决方案4】:

如果您使用的是 Visual Studio 编译器,您可以使用 /Qvec-report 命令行参数,它会告诉您哪些循环正在/没有被矢量化,并为您提供原因代码,说明它们为什么不被矢量化

循环向量化(与展开不同)是编译器使用 SIMD(SSE、SSE2、AVX)指令将循环分解为一系列并行执行的操作

https://msdn.microsoft.com/en-us/library/jj658585.aspx

gcc 和 clang 可能有类似的能力

【讨论】:

    【解决方案5】:

    您始终可以展开 for 循环。即使您不知道迭代次数,也可以使用名为 Duff's device 的技巧来完成

    另请参阅 stackoverflow 上的说明:How does Duff's device work?

    您可以使用交错的 switch 和 while 循环,让 while 循环一次处理 4 个项目。如果您想处理 6 个项目,则可以通过跳到循环处理 2+4=6 个项目中的倒数第二个项目来作弊:

    int  n = 6;
    int it = n / 4;
    int check = 0;
    switch (n % 4) {
      case 0: do { check += 1;
      case 3:      check += 1;
      case 2:      check += 1;
      case 1:      check += 1;
    } while (it--);
    }
    printf("processed %i items\n", check);
    

    【讨论】:

      猜你喜欢
      • 2017-11-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-07-21
      • 1970-01-01
      相关资源
      最近更新 更多