c++: инициализация небольших массивов и заполнение их нулями

вижу на практике, что такая конструкция

for (...)
{
    int arr[10] = {0};
    ...
}

работает быстрее, чем такая:

for (...)
{
    int arr[10];
    memset(arr, 0, sizeof(int) * 10);
    ...
}

а с чем это связано? интересно как это на уровне инструкций выглядит

и там и там сам массив скорее всего сидит в стеке, во втором случае вызывается функция для заполнения его нулями, а вот что происходит в первом случае?

правильно ли я понимаю, что при использовании оба случая идентичны относительно результата (т.е. все 10 ячеек всегда изначально заполнены 0)?


Ответы (2 шт):

Автор решения: AlexGlebe

Вот ассемблерный код :

square:
    push    rbp
    mov     rbp, rsp
    mov     DWORD PTR [rbp-52], edi
    mov     QWORD PTR [rbp-48], 0
    mov     QWORD PTR [rbp-40], 0
    mov     QWORD PTR [rbp-32], 0
    mov     QWORD PTR [rbp-24], 0
    mov     QWORD PTR [rbp-16], 0
    nop
    pop     rbp
    ret
square2:
    push    rbp
    mov     rbp, rsp
    sub     rsp, 64
    mov     DWORD PTR [rbp-52], edi
    lea     rax, [rbp-48]
    mov     edx, 40
    mov     esi, 0
    mov     rdi, rax
    call    memset
    nop
    leave
    ret

В первом варианте цикл сделан оптимизированно с раскруткой. А второй универсально функцией memset. Сама memset функция не может делать раскрутку цикла, так как длина массива неизвестна.

→ Ссылка
Автор решения: KoVadim

интересно как это на уровне инструкций выглядит

это всегда легко узнать - https://gcc.godbolt.org/ покажет все.

Внутри интересно. я заметил, что студия на уровне /O0 /O1 не выбрасывает memset. Какой бы не был он быстрый внутри, это вызов функции. А вот первый способ превращается где то в такое

xor     eax, eax
mov     ecx, 400                      ; 00000190H
rep stosb

то есть, мы процессору говорим, в регистре ecx - количество, в eax - данные, рядом там есть ещё начало массива. И вперед.

А в одно время ребята с Интела посмотрели на это сказали - а давайте это будет распознаваться процессором как некая макрокоманда (и копирование памяти через префикс rep) и будет исполнятся прям внутри, а то и контроллером памяти. И это реально быстро. Детали можно найти в этом документе https://www.intel.com/content/dam/doc/manual/64-ia-32-architectures-optimization-manual.pdf

gcc и clang для маленьких массивов просто берет и копирует регистр eax (куда предварительно записал ноль) в память без цикла - это тоже не медленно (память то в кеше).

А вот для больших массивов даже в первом случае clang вызывает memset...

Также интересно то, что gcc умеет распознавать memset как встроенную "магическую функцию" (то есть, он знает, что она делает) и заменять ее на "правильный" код.

Поэтому делаем выводы:

  • компиляторы хитры и знают много особенностей процессоров
  • если компилятор не понимает, что от него хотят - сделает общий код, что бы оно работало
  • минимальные изменения кода (размера массива) или версий компилятора могут приводить к большим перестроениям ассемблерных инструкций
→ Ссылка