在 g++ 上进行聚合初始化的 std::array 会生成大量代码

Posted

技术标签:

【中文标题】在 g++ 上进行聚合初始化的 std::array 会生成大量代码【英文标题】:std::array with aggregate initialization on g++ generates huge code 【发布时间】:2016-09-12 14:35:03 【问题描述】:

在 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++ 会生成一个简单的循环:

    删除S::S(); 删除S::S()并在课堂上初始化S::f; 移除聚合初始化(= ); 在没有-O2的情况下编译。

我完全赞成将循环展开作为一种优化,但我认为这不是一个很好的优化。在我将此报告为错误之前,有人可以确认这是否是预期的行为吗?

[编辑:我为此打开了a new bug,因为其他人似乎不匹配。它们更多的是关于较长的编译时间,而不是奇怪的代码生成。]

【问题讨论】:

哇。 g++ 在 6.1 中也是如此。我让编译器崩溃并在 Godbolt 上发出提交错误警告:godbolt.org/g/Ae75GH @NathanOliver Welp,这有点证实了这一点。谢谢。 gcc 对 constexpr 数组的处理也值得怀疑。它在初始化 constexpr std::array = make_array(...) 时会做类似的事情,其中​​ make_array() 是 constexpr。 @NathanOliver 实际上,我认为 gcc 被杀死是因为它占用了too many resources。我无法在其他任何地方重现崩溃。 @isanae 哎呀。我认为我的评论是准确的。我只是想告诉你它在 6.1 中也被破坏了。它说要提交错误报告的事实只是一个快乐的巧合。 【参考方案1】:

似乎有一个相关的错误报告,Bug 59659 - large zero-initialized std::array compile time excessive。它被认为是 4.9.0 的“固定”,所以我认为这个测试用例要么是回归,要么是补丁未覆盖的边缘用例。值得一提的是,错误报告的两个测试用例1、2 在 GCC 4.9.0 和 5.3.1 上都对我显示了症状

还有两个相关的错误报告:

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

安德鲁平斯基 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 报告这个。

【讨论】:

谢谢。虽然clang并不那么聪明。如果我将 f 初始化为 0 以外的值,它将执行 both memset 和循环。但它不会展开任何内容。 事实上,one of the comments 中用于此错误报告的测试用例仍然失败并出现类似症状。 @isanae This one 也是如此。考虑到它们在 4.9.x 上出现症状,我认为这个问题根本没有“修复”。所以这可能不是回归,而是无效修复。 @isanae 不管怎样,我进入了兔子洞,然后又收到了更多的错误报告。我认为它“未解决”并使用各种解决方法之一。 感谢您的挖掘。谢谢!【参考方案2】:

您的 GCC 错误 71165,然后与 92385 合并,已在 GCC 12 上修复。

https://gcc.godbolt.org/z/eGMq16esP

【讨论】:

以上是关于在 g++ 上进行聚合初始化的 std::array 会生成大量代码的主要内容,如果未能解决你的问题,请参考以下文章

SQL Server:在具有两列的表上进行无聚合的旋转

如何在一个列上进行分组,在另一个列上聚合数组并创建一个由分组列作为键的 JSON 对象

在思科交换机模拟软件上进行端口聚合实验,使用命令 switch(config)#interface port-group 1 却老提示错误

数据聚合与分组运算

在同一选择语句上进行多个分组

如何在 TensorFlow 上进行 Xavier 初始化