GCC将固定范围的for循环优化为具有更长、可变长度的循环。

26

我有一个POD结构体的数组,并尝试对其中一个字段进行求和。以下是最小化的示例:

struct Item
{
    int x = 0;
    int y = 0;
};

typedef Item Items[2];

struct ItemArray
{
    Items items;

    int sum_x1() const;
    int sum_x2() const;
};

int ItemArray::sum_x1() const
{
    int total = 0;
    for (unsigned ii = 0; ii < 2; ++ii)
    {
        total += items[ii].x;
    }
    return total;
}

int ItemArray::sum_x2() const
{
    int total = 0;
    for (const Item& item : items)
    {
        total += item.x;
    }
    return total;
}

这两个求和函数执行的是同一个操作,Clang编译它们时会生成相同的代码。但是在x86_64架构上,使用GCC 6编译且开启-O3优化选项时,它们并不相同。这是一个看起来良好的sum_x1()函数:

Translated text:

这两个求和函数执行的是同一个操作,Clang编译它们时会生成相同的代码。但是在x86_64架构上,使用GCC 6编译且开启-O3优化选项时,它们并不相同。这是一个看起来良好的sum_x1()函数:

  mov eax, DWORD PTR [rdi+8]
  add eax, DWORD PTR [rdi]
  ret

现在看看sum_x2()函数:

  lea rdx, [rdi+16]
  lea rcx, [rdi+8]
  xor eax, eax
  add eax, DWORD PTR [rdi]
  cmp rdx, rcx
  je .L12
  lea rcx, [rdi+16]
  add eax, DWORD PTR [rdi+8]
  cmp rdx, rcx
  je .L2
  lea rcx, [rdi+24]
  add eax, DWORD PTR [rdi+16]
  cmp rdx, rcx
  je .L2
  lea rcx, [rdi+32]
  add eax, DWORD PTR [rdi+24]
  cmp rdx, rcx
  je .L2
  lea rcx, [rdi+40]
  add eax, DWORD PTR [rdi+32]
  cmp rdx, rcx
  je .L2
  lea rcx, [rdi+48]
  add eax, DWORD PTR [rdi+40]
  cmp rdx, rcx
  je .L2
  lea rcx, [rdi+56]
  add eax, DWORD PTR [rdi+48]
  cmp rdx, rcx
  je .L2
  lea rcx, [rdi+64]
  add eax, DWORD PTR [rdi+56]
  cmp rdx, rcx
  je .L2
  lea rcx, [rdi+72]
  add eax, DWORD PTR [rdi+64]
  cmp rdx, rcx
  je .L2
  add eax, DWORD PTR [rdi+72]
  ret
.L2:
  rep ret
.L12:
  rep ret
为什么在循环长度固定为2时,GCC会发出一个长度可变的展开循环,最多可以展开10次?它只在成员函数中这样做--将sum_x2更改为自由函数可以修复它。
ICC也非常奇怪地优化了sum_x2(),尽管生成的代码完全不同。与GCC不同,无论sum_x2()是成员函数还是自由函数都没有关系--两者都不好。
我正在使用GCC 6,但所有版本的GCC似乎都存在这个问题。添加-march=haswell会使它变得更糟,增加数组大小为2的元素的迭代次数达到15。 GCC 5和7生成更复杂的代码,添加了SIMD指令。 我想确定此问题的确切原因,以便我可以找到并修复类似的情况。了解在GCC 6中触发此行为的原因将非常有帮助。 我的代码中有很多基于范围的for循环,如果GCC不能生成合理的代码,我将别无选择。
尝试一下:https://godbolt.org/g/9GK4jy 更多相关的疯狂:https://godbolt.org/g/BGYggD(最优代码为3条指令;GCC 6生成8条指令;GCC 7生成130条指令)

2
我猜这是gcc中的一个错误。您可以在此处https://gcc.gnu.org/bugzilla/报告它,并标记为未优化。 - fghj
2
根据我的经验,当分析错误时,“gcc人员”告诉您根本原因和可能的解决方法的概率比您在SO上得到答案的概率要高。顺便问一下,在您创建错误报告后,您能否将错误报告链接添加到您的问题中? - fghj
3
我已经在GCC bug https://gcc.gnu.org/bugzilla/show_bug.cgi?id=81719 上提出了问题。我仍然希望能够得到一个答案(在这里或那里),以了解是什么触发了这个问题,这样我就可以诊断是否可能在我的程序中的其他地方也出现了这个问题。 - John Zwinck
1
在您的第二个示例中,非平凡析构函数是罪魁祸首。如果您将其删除或替换为~FixedArray() noexcept = default;,那么两个gcc版本都会生成3条指令。 - Praetorian
1
@Praetorian:我知道你的意思,第二个链接确实有一个用户定义的析构函数,如果没有这个函数,问题就不会出现。然而,这样的析构函数存在(它是noexcept、内联定义,并且什么也不做)不应该改变实施构造所需的代码。但事实上确实改变了。 - John Zwinck
显示剩余4条评论
1个回答

3
如 Richard Biener 在我的错误报告中所述,问题似乎在于GCC 8版本之前无法理解类或结构体的字段与普通变量一样受到相同的优化(例如常数循环次数)的影响。因此,即使在已知成员变量的情况下,它仍会发出各种花哨的代码来最优地循环未知次数的循环体。

据我理解,这个错误可能会影响到一些现有的代码,例如,在任何成员小数组是C++11范围为基础的循环体中。

感谢Richard Biener及时解决此问题(目标是GCC 8)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接