Чому ініціалізація масиву 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замість inlining rep stos)


Примітка редактора: запитання спочатку мало неоптимізований вихід компілятора, який працював так само, але неефективний код -O0не підтверджує нічого. Але виявляється, що цю оптимізацію GCC пропускає навіть при -O3.

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

Використання volatile a[100]{1,2,3,4}отримує GCC для створення, а потім копіювання масиву, який є божевільним. Зазвичай volatileце добре для того, щоб подивитися, як компілятори вставляють локальні змінні або викладають їх у стек.


1
@Damien Ви неправильно зрозуміли моє запитання. Я запитую, чому, наприклад, [0] присвоюється значення вдвічі, як якщо a[0] = 0;і потім a[0] = 1;.
Лассі

1
Я не в змозі прочитати збірку, але де це показує, що масив повністю заповнений нулями?
smac89

3
Ще один цікавий факт: для більшої кількості елементів, ініціалізованих, і gcc, і кланг повертаються до копіювання всього масиву з .rodata... Я не можу повірити, що копіювання 400 байтів швидше, ніж нулювання та встановлення 8 елементів.
Шут

2
Ви відключили оптимізацію; неефективний код не дивно, поки ви не переконаєтесь, що те саме відбувається і при -O3(що це робиться). godbolt.org/z/rh_TNF
Пітер Кордес

12
Що ще ти хочеш знати? Це пропущена оптимізація. Повідомте про помилку GCC з missed-optimizationключовим словом.
Пітер Кордес

Відповіді:


2

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

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

тож може бути більш ефективним у сенсі кешу та оптимізабельності спочатку нуль всього блоку пам'яті, а потім встановлення окремих значень.

Можливо, зміни поведінки залежать від:

  • цільова архітектура
  • цільова ОС
  • довжина масиву
  • коефіцієнт ініціалізації (явно ініціалізовані значення / довжина)
  • позиції ініціалізованих значень

Звичайно, у вашому випадку ініціалізація ущільнена на початку масиву, і оптимізація була б тривіальною.

Тож здається, що тут GCC робить найбільш загальний підхід. Схоже на відсутність оптимізації.


Так, оптимальною стратегією цього коду, мабуть, було б нульове все, а може, просто все, починаючи a[6]далі від ранніх прогалин, заповнених єдиними запасами безпосередніх або нульових. Особливо, якщо націлено на x86-64, щоб ви могли використовувати магазини qword, щоб робити 2 елементи одночасно, нижній - ненульовий. наприклад, mov QWORD PTR [rsp+3*4], 1зробити елементи 3 і 4 з одним нерівним сховищем qword.
Пітер Кордес

Теорія поведінки може теоретично залежати від цільової ОС, але у фактичних GCC це не буде і не має підстав для цього. Тільки цільова архітектура (і всередині цього параметри налаштування різних мікроархітектур, наприклад -march=skylakevs. -march=k8vs. -march=knl, взагалі були б дуже різними і, можливо, з точки зору відповідної стратегії для цього.)
Пітер Кордес,

Це навіть дозволено в C ++? Я думав, що це лише C.
Лассі

@Lassie Ви маєте рацію в c ++, це заборонено, але питання більше стосується бекенда компілятора, так що це не має великого значення. також показаний код може бути обом
vlad_tepesch

Ви навіть можете легко сконструювати приклади, які працюють однаково в C ++, оголосивши деякі struct Bar{ int i; int a[100]; int j;} та ініціалізуючи Bar a{1,{2,3,4},4};gcc робить те саме: нуль все викреслити, а потім встановити 5 значень
vlad_tepesch
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.