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

标签 c++ optimization g++ stdarray loop-unrolling

在 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 的初始化展开,生成 4096 movl指示:
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为此,因为其他人似乎不匹配。他们更多的是关于长编译时间而不是奇怪的代码生成。]

    最佳答案

    似乎有一个相关的错误报告,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

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

    This is most likely a memory hog which is generating lots of default constructors rather than a loop over them.



    那个声称是这个的复制品:

    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

    Generating the array initialization for this constexpr constructor is the problem:

      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。

    关于c++ - 在 g++ 上进行聚合初始化的 std::array 生成大量代码,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/67418273/

    相关文章:

    .net - 为什么我的VB.NET类库在Reflector中显示 “My”和 “My.Resources” namespace ?

    sql - 每页加载 20 个 SQL 查询真的很重要吗?

    c++ - "cpp-bin"在linux中是什么意思?

    linux - 我无法安装 g++

    c++ - 如何用 cvmGet 替换对 cvGetReal2D 的调用?

    c++ - 如何让 CMake 使用 Mingw-w64 gcc/g++?

    c++ - 如何索引转换后的用户数据值?

    c++ - 在cmake文件中使用edje_cc

    c++ - 如何使用 C++ 在 linux 终端中显示 unicode 字符?

    引用类型转换中的 C++ 虚函数