std::array с агрегатной инициализацией на g++ генерирует огромный код

В 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++ создаст простой цикл:

  1. Удалить S::S();
  2. Удалите S::S() и инициализируйте S::f в классе;
  3. Удалить агрегатную инициализацию (= {});
  4. Скомпилировать без -O2.

Я полностью за развертывание цикла в качестве оптимизации, но я не думаю, что это очень хорошо. Прежде чем я сообщу об этом как об ошибке, может ли кто-нибудь подтвердить, является ли это ожидаемым поведением?

[править: я открыл для этого новую ошибку, потому что другие вроде не совпадают. Они были больше связаны с долгим временем компиляции, чем со странным codegen.]


person isanae    schedule 16.05.2016    source источник
comment
Ух ты. g++ делает это и в 6.1. У меня произошел сбой компилятора и выдано предупреждение об ошибке отправки на godbolt: godbolt.org/g/Ae75GH   -  person NathanOliver    schedule 16.05.2016
comment
@NathanOliver Хорошо, это как бы подтверждает это. Спасибо.   -  person isanae    schedule 16.05.2016
comment
Обработка gcc массива constexpr также вызывает подозрения. То же самое происходит при инициализации constexpr std::array‹char, N› = make_array(...), где make_array() — это constexpr.   -  person Richard Hodges    schedule 16.05.2016
comment
@NathanOliver На самом деле, я думаю, что gcc убит, потому что он занимает слишком много ресурсов. Я не мог воспроизвести сбой в другом месте.   -  person isanae    schedule 16.05.2016
comment
@isanae Ой. Я думаю, что мой комментарий был полностью точен. Я просто хотел показать вам, что он также был сломан в 6.1. Тот факт, что он сказал подать отчет об ошибке, был просто счастливым совпадением.   -  person NathanOliver    schedule 16.05.2016


Ответы (1)


Похоже, есть связанный отчет об ошибке, Ошибка 59659 - слишком большое время компиляции std::array с нулевой инициализацией< /а>. Это считалось «исправленным» для 4.9.0, поэтому я считаю этот тест либо регрессом, либо пограничным случаем, не охваченным патчем. Что бы это ни стоило, два тестовых примера отчета об ошибке1, 2 у меня проявляются симптомы как на GCC 4.9.0, так и на 5.3 .1

Есть еще два связанных отчета об ошибке:

Ошибка 68203 - О бесконечном времени компиляции в структуре с вложенным массивом пар с -std=c++11

Эндрю Пински 2015-11-04 07:56:57 UTC

Скорее всего, это пожиратель памяти, который генерирует множество конструкторов по умолчанию, а не зацикливается на них.

Тот утверждает, что является дубликатом этого:

Ошибка 56671: Gcc использует большие объемы памяти и мощности процессора с большими наборами битов C++11

Джонатан Уэйкли 26-01-2016 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 секунды. Я обязательно сообщу об этом их багзилле.

person user6342117    schedule 16.05.2016
comment
Спасибо. clang не такой уж умный. Если я инициализирую f значением, отличным от 0, будет выполнено и memset, и цикл. Но ничего не разворачивает. - person isanae; 16.05.2016
comment
На самом деле тестовый пример в одного из комментариев для этот отчет об ошибке все еще не работает с аналогичными симптомами. - person isanae; 16.05.2016
comment
@isanae этого тоже. Я не думаю, что проблема вообще была исправлена, учитывая, что они проявляют симптомы на 4.9.x. Так что, вероятно, это не регрессия, а недопустимое исправление. - person user6342117; 16.05.2016
comment
@isanae В любом случае, я пошел в кроличью нору и только что вернулся с новыми отчетами об ошибках. Я бы посчитал это нерешенным и использовал один из различных обходных путей. - person user6342117; 16.05.2016
comment
Я ценю раскопки. Спасибо! - person isanae; 16.05.2016