无法优化看似明显的循环不变性(但易变的限定词确实很神奇)

时间:2018-08-06 10:46:36

标签: c++ compiler-optimization loop-invariant

Godbolt链接: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

为什么编译器不将subarraysubsubarray计算移到内部循环之外?


随机volatile做魔术

我随机添加了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使用-O3 -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

循环不变式从内部循环中推出。随机volatile做了什么以使GCC优化不变量?当 clang 6.0.0 时,优化不会发生。

1 个答案:

答案 0 :(得分:2)

这与解决问题的随机挥发无关-问题更深。

您已经猜到问题确实与“ y”有关

检查此示例:

using System.Collections.ObjectModel;
using System.Diagnostics;
using Windows.UI.Xaml.Controls;

namespace PlexHelper
{
    public sealed partial class MainPage : Page
    {
        public ObservableCollection<Show> Shows { get; } = new ObservableCollection<Show>();
        public Show SelectedShow { get; set; }

        public MainPage()
        {
            this.InitializeComponent();
            Shows.Add(new Show("Show 1"));
            Shows.Add(new Show("Show 2"));
            Shows.Add(new Show("Show 3"));
            SelectedShow = Shows[0];
        }

        private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            Debug.WriteLine("now selected: " + SelectedShow.Name);
        }
    }
}

我使用除法技巧来避免硬优化(gcc可以评估所有循环并直接以纯赋值形式提供y;使用加,减或乘运算时,它也会展开最内层的循环-请玩弄godbolt,看看它如何外观)

现在反汇编如下: https://godbolt.org/g/R1EGSb

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace PlexHelper
{
    public class Show : INotifyPropertyChanged
    {
        private string _name;
        public string Name
        {
            get => _name;
            set
            {
                if (_name != value)
                {
                    _name = value;
                    OnPropertyChanged();
                }
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        public Show(string name = "Default Show Name")
        {
            Name = name;
        }

        public void OnPropertyChanged([CallerMemberName] string propertyName = null) =>
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

.L2是最内层的循环,因此代码看起来像预期的那样-子数组和子子数组已预先计算。

所以您可能想知道-为什么当“ y”是局部的时,一切正常,而全局不是。

要明确-不必在main中声明“ 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;
}

或使用命名空间

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

,然后将y称为wtf :: y;

还是很好。

所有内容都简化为别名。要查看它,首先将y更改为指针:

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

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

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

但是,如果您“保证”这些地址不重叠,优化将返回。

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

TL; DR;

如果您使用的是指针编译器,则可能无法证明地址没有别名并且将使用安全路径。如果您100%确定他们没有使用 restrict 来告知这一事实。