代码之家  ›  专栏  ›  技术社区  ›  Yashas πάντα ῥεῖ

未能优化看似明显的循环不变量(但volatile限定符有魔力)

  •  3
  • Yashas πάντα ῥεῖ  · 技术社区  · 7 年前

    螺栓连接: https://godbolt.org/g/Hv6MAL

    typedef int cell;
    
    cell y;
    const cell *phys_addr = (const cell*)0x12340;
    int main() {
        for (int i = 0; i < 20; i++) {
            for (int j = 0; j < 30; j++) {
                for (int k = 0; k < 50; k++) {
                    const cell *subarray = (&phys_addr[i] + phys_addr[i]/sizeof(cell));
                    const cell *subsubarray = (&subarray[j] + subarray[j]/sizeof(cell));
                    y = subsubarray[k];
                }
            } 
        }
    }
    

    期望编译器将上述代码优化为类似于:

    int main() {
        for (int i = 0; i < 20; i++) {
            const cell *subarray = (&phys_addr[i] + phys_addr[i]/sizeof(cell));
            for (int j = 0; j < 30; j++) {
                const cell *subsubarray = (&subarray[j] + subarray[j]/sizeof(cell));
                for (int k = 0; k < 50; k++) {
                    y = subsubarray[k];
                }
            } 
        }
    }
    

    但是gcc 8.2生成的程序集 -O3 -m32 标志是:

      push ebp
      push edi
      push esi
      push ebx
      sub esp, 8
      mov eax, DWORD PTR phys_addr
      mov DWORD PTR [esp], 0
      mov DWORD PTR [esp+4], eax
      mov ebp, eax
    .L4:
      xor esi, esi
    .L3:
      lea edi, [0+esi*4]
      xor eax, eax
    .L2:
      mov edx, DWORD PTR [ebp+0]
      mov ecx, DWORD PTR [esp+4]
      shr edx, 2
      add edx, DWORD PTR [esp]
      lea ebx, [ecx+edx*4]
      lea edx, [eax+esi]
      add eax, 1
      mov ecx, DWORD PTR [ebx+edi]
      shr ecx, 2
      add edx, ecx
      mov edx, DWORD PTR [ebx+edx*4]
      mov DWORD PTR y, edx
      cmp eax, 50
      jne .L2
      add esi, 1
      cmp esi, 30
      jne .L3
      add DWORD PTR [esp], 1
      mov eax, DWORD PTR [esp]
      add ebp, 4
      cmp eax, 20
      jne .L4
      add esp, 8
      xor eax, eax
      pop ebx
      pop esi
      pop edi
      pop ebp
      ret
    

    为什么编译器不移动 subarray subsubarray 在内环之外的计算?


    随机的 volatile 施魔法

    我随机添加 不稳定的 为了防止DCE删除所有代码,然后不知何故将循环不变量从内部循环中取出。

    int main() {
        for (int i = 0; i < 20; i++) {
            for (int j = 0; j < 30; j++) {
                for (int k = 0; k < 50; k++) {
                    const cell *subarray = (&phys_addr[i] + phys_addr[i]/sizeof(cell));
                    const cell *subsubarray = (&subarray[j] + subarray[j]/sizeof(cell));
                    volatile cell y = subsubarray[k];
                }
            } 
        }
        return 0;
    }
    

    这主要不是因为 y 使用后成为局部变量 std::cout << subsubarray[k]; 阻止了优化。

    gcc 8.2生成的程序集 -臭氧-m32 由于上述代码的标志是:

    main:
      push ebp
      push edi
      xor edi, edi
      push esi
      push ebx
      sub esp, 20
      mov ebp, DWORD PTR phys_addr
    .L4:
      mov eax, DWORD PTR [ebp+0+edi*4]
      xor ecx, ecx
      shr eax, 2
      add eax, edi
      lea ebx, [ebp+0+eax*4]
      lea esi, [ebx+200]
    .L3:
      mov edx, DWORD PTR [ebx+ecx*4]
      mov DWORD PTR [esp], ecx
      shr edx, 2
      add edx, ecx
      sal edx, 2
      lea eax, [ebx+edx]
      add edx, esi
    .L2:
      mov ecx, DWORD PTR [eax]
      add eax, 4
      mov DWORD PTR [esp+16], ecx
      cmp edx, eax
      jne .L2
      mov ecx, DWORD PTR [esp]
      add ecx, 1
      cmp ecx, 30
      jne .L3
      add edi, 1
      cmp edi, 20
      jne .L4
      add esp, 20
      xor eax, eax
      pop ebx
      pop esi
      pop edi
      pop ebp
      ret
    

    循环不变量从内部循环中推出。随机的是什么 不稳定的 是否允许GCC优化不变量?优化不会发生在 叮当6.0.0 .

    1 回复  |  直到 7 年前
        1
  •  2
  •   Anty    7 年前

    它不是关于随机的挥发性固定你的问题-问题更深。

    正如你们已经猜到的,这个问题确实和“y”有关

    检查此示例:

    typedef int cell;
    
    const cell *phys_addr = (const cell*)0x12340;
    
    int main() {
        cell y = 1;
        for (int i = 0; i < 20; i++) {
            for (int j = 0; j < 30; j++) {
                for (int k = 0; k < 50; k++) {
                    const cell *subarray = (&phys_addr[i] + phys_addr[i]/sizeof(cell));
                    const cell *subsubarray = (&subarray[j] + subarray[j]/sizeof(cell));
                    y /= subsubarray[k];
                }
            } 
        }
        return y;
    }
    

    我使用了除法技巧来避免硬优化(gcc可以计算所有循环,并直接以简单的赋值方式提供y;当使用add、sub或multiply时,它也将展开最里面的循环-请在godbolt中播放以查看其外观)

    现在反汇编看起来是这样的: https://godbolt.org/g/R1EGSb

    main:
      push ebp
      push edi
      push esi
      push ebx
      sub esp, 12
      mov eax, DWORD PTR phys_addr
      mov DWORD PTR [esp], 0
      mov DWORD PTR [esp+4], eax
      mov eax, 1
    .L4:
      mov esi, DWORD PTR [esp]
      mov edi, DWORD PTR [esp+4]
      mov edx, DWORD PTR [edi+esi*4]
      mov DWORD PTR [esp+8], edx
      shr edx, 2
      add edx, esi
      xor esi, esi
      lea edi, [edi+edx*4]
      lea ebp, [edi+200]
    .L3:
      mov ebx, DWORD PTR [edi+esi*4]
      shr ebx, 2
      add ebx, esi
      sal ebx, 2
      lea ecx, [edi+ebx]
      add ebx, ebp
    .L2:
      cdq
      idiv DWORD PTR [ecx]
      add ecx, 4
      cmp ebx, ecx
      jne .L2
      add esi, 1
      cmp esi, 30
      jne .L3
      add DWORD PTR [esp], 1
      mov edi, DWORD PTR [esp]
      cmp edi, 20
      jne .L4
      add esp, 12
      pop ebx
      pop esi
      pop edi
      pop ebp
      ret
    phys_addr:
      .long 74560
    

    .L2是最里面的循环,所以代码看起来像预期的-子数组和子数组是在前面预先计算的。

    所以你可能会想-为什么当“y”是本地的时候一切都好,而当全局的时候不是。

    为了清楚起见,“y”不必在main中声明。它可以像这样静止

    static cell y;
    const cell * __restrict__ phys_addr = (const cell*)0x12340;
    

    或使用命名空间

    namespace wtf{ cell y; }
    const cell * __restrict__ phys_addr = (const cell*)0x12340;
    

    把y称为wtf::y;

    还不错。

    全部压缩为别名。要查看它,我们先将y改为pointer:

    typedef int cell;
    
    cell *  y;
    const cell *  phys_addr = (const cell*)0x12340;
    
    int main() {
        cell ylocal;
        y = &ylocal;
        for (int i = 0; i < 20; i++) {
            for (int j = 0; j < 30; j++) {
                for (int k = 0; k < 50; k++) {
                    const cell *subarray = (&phys_addr[i] + phys_addr[i]/sizeof(cell));
                    const cell *subsubarray = (&subarray[j] + subarray[j]/sizeof(cell));
                    *y /= subsubarray[k];
                }
            } 
        }
        return *y;
    }
    

    不再进行循环优化。。。。

    可以假设y和phys addr重叠-写入y可能会修改一些内存单元,因此所有字典都必须使用最新数据计算(physaddr中的const表示只有指针不应该修改内存,而不是全局只读)。

    但如果你“保证”这些地址不重叠优化就会回来。

    typedef int cell;
    
    cell * __restrict__ y;
    const cell * __restrict__ phys_addr = (const cell*)0x12340;
    
    int main() {
        cell ylocal;
        y = &ylocal;
        for (int i = 0; i < 20; i++) {
            for (int j = 0; j < 30; j++) {
                for (int k = 0; k < 50; k++) {
                    const cell *subarray = (&phys_addr[i] + phys_addr[i]/sizeof(cell));
                    const cell *subsubarray = (&subarray[j] + subarray[j]/sizeof(cell));
                    *y /= subsubarray[k];
                }
            } 
        }
        return *y;
    }
    

    TL;博士;

    限制 告诉它这个事实。