Почему GCC агрегирует инициализацию массива в первую очередь с нулями, включая ненулевые элементы?

21

Почему gcc заполняет весь массив нулями вместо оставшихся 96 целых чисел? Все ненулевые инициализаторы находятся в начале массива.

void *sink;
void bar() {
    int a[100]{1,2,3,4};
    sink = a;             // a escapes the function
    asm("":::"memory");   // and compiler memory barrier
    // forces the compiler to materialize a[] in memory instead of optimizing away
}

MinGW8.1 и gcc9.2 оба создают asm вот так ( проводник компилятора Godbolt ).

# gcc9.2 -O3 -m32 -mno-sse
bar():
    push    edi                       # save call-preserved EDI which rep stos uses
    xor     eax, eax                  # eax=0
    mov     ecx, 100                  # repeat-count = 100
    sub     esp, 400                  # reserve 400 bytes on the stack
    mov     edi, esp                  # dst for rep stos
        mov     DWORD PTR sink, esp       # sink = a
    rep stosd                         # memset(a, 0, 400) 

    mov     DWORD PTR [esp], 1        # then store the non-zero initializers
    mov     DWORD PTR [esp+4], 2      # over the zeroed part of the array
    mov     DWORD PTR [esp+8], 3
    mov     DWORD PTR [esp+12], 4
 # memory barrier empty asm statement is here.

    add     esp, 400                  # cleanup the stack
    pop     edi                       # and restore caller's EDI
    ret

(с включенным SSE он скопирует все 4 инициализатора с загрузкой / хранением movdqa)

Почему GCC не делает lea edi, [esp+16]и устанавливает (с rep stosd) только последние 96 элементов, как это делает Кланг? Это пропущенная оптимизация или так эффективнее? (На самом деле звонит Clang memsetвместо того, чтобы вставлять rep stos)


Примечание редактора: изначально вопрос содержал неоптимизированный вывод компилятора, который работал таким же образом, но неэффективный код -O0ничего не доказывает. Но оказывается, что эта оптимизация пропущена GCC даже в -O3.

Передача указателя на aне встроенную функцию была бы другим способом заставить компилятор материализоваться a[], но в 32-битном коде, который приводит к значительному загромождению asm. (Аргументы стека приводят к толчкам, которые смешиваются с хранилищами в стеке для инициализации массива.)

Использование volatile a[100]{1,2,3,4}получает GCC для создания, а затем скопировать массив, что безумие. Обычно volatileполезно посмотреть, как компиляторы инициируют локальные переменные или размещают их в стеке.

девчурка
источник
1
@Damien Вы неправильно поняли мой вопрос. Я спрашиваю, почему, например, a [0] присваивается значение дважды, как если бы a[0] = 0;и тогда a[0] = 1;.
Ласси
1
Я не могу прочитать сборку, но где это показывает, что массив заполнен полностью нулями?
smac89
3
Еще один интересный факт: для инициализации большего количества элементов и gcc, и clang возвращаются к копированию всего массива из .rodata... Я не могу поверить, что копирование 400 байтов происходит быстрее, чем обнуление и установка 8 элементов.
Шут
2
Вы отключили оптимизацию; неэффективный код не удивителен, пока вы не убедитесь, что происходит то же самое -O3(что и происходит). godbolt.org/z/rh_TNF
Питер Кордес
12
Что еще ты хочешь знать? Это пропущенная оптимизация, иди сообщай об этом в bugzilla GCC с missed-optimizationключевым словом.
Питер Кордес

Ответы:

2

Теоретически ваша инициализация может выглядеть так:

int a[100] = {
  [3] = 1,
  [5] = 42,
  [88] = 1,
};

так что может быть более эффективным в смысле кэша и оптимизируемости сначала обнулить весь блок памяти, а затем установить отдельные значения.

Может быть поведение меняется в зависимости от:

  • целевая архитектура
  • целевая ОС
  • длина массива
  • коэффициент инициализации (явно инициализированные значения / длина)
  • позиции инициализированных значений

Конечно, в вашем случае инициализация сжимается в начале массива, и оптимизация будет тривиальной.

Таким образом, похоже, что gcc использует наиболее общий подход. Похоже на отсутствующую оптимизацию.

vlad_tepesch
источник
Да, оптимальной стратегией для этого кода, вероятно, было бы обнуление всего, или, может быть, всего, начиная a[6]с ранних пробелов, заполненных единичными запасами мгновенных значений или нулей. Особенно если вы нацелены на x86-64, так что вы можете использовать хранилища qword, чтобы делать 2 элемента одновременно, а нижний ненулевой. например, mov QWORD PTR [rsp+3*4], 1чтобы сделать элементы 3 и 4 с одним смещенным хранилищем слов.
Питер Кордес
Поведение в теории может зависеть от целевой ОС, но в реальном GCC это не так, и нет причин для этого. Только целевая архитектура (и в этом случае параметры настройки для разных микроархитектур, например, -march=skylakeпротив и -march=k8против -march=knl, в целом будут сильно отличаться, и, возможно, с точки зрения подходящей стратегии для этого.)
Питер Кордес,
Это даже разрешено в C ++? Я думал, что это только C.
Ласси
@ Лесси, вы правы в c ++, это не разрешено, но вопрос больше связан с бэкэндом компилятора, так что это не имеет большого значения. также показанный код может быть как
vlad_tepesch
Вы даже можете легко создать примеры, которые работают одинаково в C ++, объявив некоторые из них struct Bar{ int i; int a[100]; int j;} и инициализировав Bar a{1,{2,3,4},4};gcc,
сделав