c++: инициализация небольших массивов и заполнение их нулями
вижу на практике, что такая конструкция
for (...)
{
int arr[10] = {0};
...
}
работает быстрее, чем такая:
for (...)
{
int arr[10];
memset(arr, 0, sizeof(int) * 10);
...
}
а с чем это связано? интересно как это на уровне инструкций выглядит
и там и там сам массив скорее всего сидит в стеке, во втором случае вызывается функция для заполнения его нулями, а вот что происходит в первом случае?
правильно ли я понимаю, что при использовании оба случая идентичны относительно результата (т.е. все 10 ячеек всегда изначально заполнены 0)?
Ответы (2 шт):
Вот ассемблерный код :
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 функция не может делать раскрутку цикла, так как длина массива неизвестна.
интересно как это на уровне инструкций выглядит
это всегда легко узнать - 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 как встроенную "магическую функцию" (то есть, он знает, что она делает) и заменять ее на "правильный" код.
Поэтому делаем выводы:
- компиляторы хитры и знают много особенностей процессоров
- если компилятор не понимает, что от него хотят - сделает общий код, что бы оно работало
- минимальные изменения кода (размера массива) или версий компилятора могут приводить к большим перестроениям ассемблерных инструкций