使用g++进行聚合初始化的std::array生成了大量的代码

25

在g++ 4.9.2和5.3.1上,这段代码需要几秒钟来编译,并且产生一个大小为52,776字节的可执行文件:

#include <array>
#include <iostream>

int main()
{
    constexpr std::size_t size = 4096;

    struct S
    {
        float f;
        S() : f(0.0f) {}
    };

    std::array<S, size> a = {};  // <-- note aggregate initialization

    for (auto& e : a)
        std::cerr << e.f;

    return 0;
}

增加size似乎会线性地增加编译时间和可执行文件大小。我无法通过clang 3.5或Visual C++ 2015复现此行为。使用-Os没有任何区别。

$ time g++ -O2 -std=c++11 test.cpp
real    0m4.178s
user    0m4.060s
sys     0m0.068s

检查汇编代码可以发现,a 的初始化是展开的,生成了 4096movl 指令:

main:
.LFB1313:
    .cfi_startproc
    pushq   %rbx
    .cfi_def_cfa_offset 16
    .cfi_offset 3, -16
    subq    $16384, %rsp
    .cfi_def_cfa_offset 16400
    movl    $0x00000000, (%rsp)
    movl    $0x00000000, 4(%rsp)
    movq    %rsp, %rbx
    movl    $0x00000000, 8(%rsp)
    movl    $0x00000000, 12(%rsp)
    movl    $0x00000000, 16(%rsp)
       [...skipping 4000 lines...]
    movl    $0x00000000, 16376(%rsp)
    movl    $0x00000000, 16380(%rsp)

只有当T具有非平凡构造函数并且使用{}初始化数组时才会发生这种情况。 如果我执行以下任何操作,g ++将生成一个简单的循环:
  1. 删除S :: S()
  2. 删除S :: S()并在类内初始化S :: f
  3. 删除聚合初始化(= {});
  4. 不使用-O2编译。
我赞成循环展开作为优化,但我认为这不是一个很好的优化。 在我将其报告为错误之前,可以有人确认这是否是预期行为吗?
[编辑:我已经为此打开了a new bug,因为其他问题似乎不匹配。 它们更多地涉及长时间编译而不是奇怪的代码生成。]

4
哇, g++ 6.1版本也能做到这一点。我让编译器在godbolt上崩溃并发出一个提交错误警告:https://godbolt.org/g/Ae75GH - NathanOliver
@NathanOliver 好吧,那就证实了。谢谢。 - isanae
2
gcc 对于 constexpr 数组的处理也值得怀疑。当初始化一个 constexpr std::array<char, N> = make_array(...),其中 make_array() 是 constexpr 时,它会做类似的事情。 - Richard Hodges
@NathanOliver 实际上,我认为gcc被杀死是因为它正在占用过多的资源。我在其他地方无法重现崩溃。 - isanae
@isanae 噢。我想我的评论是完全准确的。我只是想向您展示它在6.1中也存在问题。要求提交错误报告只是一个愉快的巧合。 - NathanOliver
固定于GCC 12 https://gcc.gnu.org/bugzilla/show_bug.cgi?id=92385#c13 - Giovanni Cerretani
2个回答

17

似乎有一个相关的 bug 报告,Bug 59659 - large zero-initialized std::array compile time excessive。它被认为是在 4.9.0 中“修复”了,所以我认为这个测试用例要么是一个回归,要么是补丁未覆盖到的边缘情况。值得一提的是,两个 bug 报告的测试用例1, 2 在我使用的 GCC 4.9.0 和 5.3.1 上都出现了症状。

还有两个相关的 bug 报告:

Bug 68203 - Аbout infinite compilation time on struct with nested array of pairs with -std=c++11

Andrew Pinski 2015-11-04 07:56:57 UTC

这很可能是一个消耗大量内存生成许多默认构造函数而不是循环的内存占用问题。

那个报告声称是这个报告的重复:

Bug 56671 - Gcc uses large amounts of memory and processor power with large C++11 bitsets

Jonathan Wakely 2016-01-26 15:12:27 UTC

生成这个 constexpr 构造函数的数组初始化是问题所在:

  constexpr _Base_bitset(unsigned long long __val) noexcept
  : _M_w{ _WordT(__val)
   } { }

实际上,如果我们将其更改为S a[4096] {};,就不会出现问题。


使用perf我们可以看到GCC花费了大部分时间。首先:

perf record g++ -std=c++11 -O2 test.cpp

然后是perf report

  10.33%  cc1plus   cc1plus                 [.] get_ref_base_and_extent
   6.36%  cc1plus   cc1plus                 [.] memrefs_conflict_p
   6.25%  cc1plus   cc1plus                 [.] vn_reference_lookup_2
   6.16%  cc1plus   cc1plus                 [.] exp_equiv_p
   5.99%  cc1plus   cc1plus                 [.] walk_non_aliased_vuses
   5.02%  cc1plus   cc1plus                 [.] find_base_term
   4.98%  cc1plus   cc1plus                 [.] invalidate
   4.73%  cc1plus   cc1plus                 [.] write_dependence_p
   4.68%  cc1plus   cc1plus                 [.] estimate_calls_size_and_time
   4.11%  cc1plus   cc1plus                 [.] ix86_find_base_term
   3.41%  cc1plus   cc1plus                 [.] rtx_equal_p
   2.87%  cc1plus   cc1plus                 [.] cse_insn
   2.77%  cc1plus   cc1plus                 [.] record_store
   2.66%  cc1plus   cc1plus                 [.] vn_reference_eq
   2.48%  cc1plus   cc1plus                 [.] operand_equal_p
   1.21%  cc1plus   cc1plus                 [.] integer_zerop
   1.00%  cc1plus   cc1plus                 [.] base_alias_check

这可能对除GCC开发人员以外的人意义不大,但看到占用了这么多编译时间的内容仍然很有趣。


Clang 3.7.0在这方面做得比GCC好多了。在-O2下,它只需要不到一秒钟的编译时间,产生一个更小的可执行文件(8960字节)和这样的汇编代码:

0000000000400810 <main>:
  400810:   53                      push   rbx
  400811:   48 81 ec 00 40 00 00    sub    rsp,0x4000
  400818:   48 8d 3c 24             lea    rdi,[rsp]
  40081c:   31 db                   xor    ebx,ebx
  40081e:   31 f6                   xor    esi,esi
  400820:   ba 00 40 00 00          mov    edx,0x4000
  400825:   e8 56 fe ff ff          call   400680 <memset@plt>
  40082a:   66 0f 1f 44 00 00       nop    WORD PTR [rax+rax*1+0x0]
  400830:   f3 0f 10 04 1c          movss  xmm0,DWORD PTR [rsp+rbx*1]
  400835:   f3 0f 5a c0             cvtss2sd xmm0,xmm0
  400839:   bf 60 10 60 00          mov    edi,0x601060
  40083e:   e8 9d fe ff ff          call   4006e0 <_ZNSo9_M_insertIdEERSoT_@plt>
  400843:   48 83 c3 04             add    rbx,0x4
  400847:   48 81 fb 00 40 00 00    cmp    rbx,0x4000
  40084e:   75 e0                   jne    400830 <main+0x20>
  400850:   31 c0                   xor    eax,eax
  400852:   48 81 c4 00 40 00 00    add    rsp,0x4000
  400859:   5b                      pop    rbx
  40085a:   c3                      ret    
  40085b:   0f 1f 44 00 00          nop    DWORD PTR [rax+rax*1+0x0]

另一方面,使用GCC 5.3.1,没有进行任何优化的情况下,编译速度非常快,但仍会生成一个大小为95328的可执行文件。使用-O2编译可以将可执行文件大小减小到53912,但编译时间需要4秒钟。我一定会向他们的bugzilla报告这个问题。


1
谢谢。clang并不是那么聪明。如果我将f初始化为0以外的值,它将执行同时memset和循环。但它不会展开任何东西。 - isanae
事实上,这个错误报告中的其中一条评论中的一个测试用例仍然出现类似症状的失败。 - isanae
2
@isanae,这个链接也是。我认为问题根本没有被“修复”,因为它们在4.9.x上表现出症状。所以这可能不是一个回归,而是一个无效的修复。 - user6342117
1
@isanae 无论如何,我进入了兔子洞,刚刚又回来带着更多的错误报告。我会把它视为“未解决”,并使用各种解决方法之一。 - user6342117
我很感激你的挖掘。谢谢! - isanae

1

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