Ответ 1
Почему для 32-разрядной системы требуется в 4 раза больше времени для сохранения 32-битных значений, чем для сохранения 8-битных значений?
Это не так. Но есть три разных проблемы с вашим эталоном, которые дают вам эти результаты.
- Вы не забудете память. Таким образом, вы проверяете ошибки в массивах во время теста. Эти ошибки страниц вместе с взаимодействием ядра ОС являются доминирующим фактором времени.
- Компилятор с
-O3
полностью побеждает ваш тест, преобразовывая все ваши циклы вmemset()
. - Ваш тест связан с памятью. Таким образом, вы измеряете скорость своей памяти вместо ядра.
Проблема 1: тестовые данные не префотопируются
Ваши массивы объявляются, но не используются перед эталоном. Из-за того, как работает ядро и память, они еще не отображаются в память. Это происходит только тогда, когда вы сначала прикасаетесь к ним. И когда это происходит, это наносит очень большой штраф из ядра, чтобы отобразить страницу.
Это можно сделать, коснувшись всех массивов перед эталоном.
No Pre-Fault: http://coliru.stacked-crooked.com/a/1df1f3f9de420d18
g++ -O3 -Wall main.cpp && ./a.out
Time of processing int8 array: 28983us.
Time of processing int16 array: 57100us.
Time of processing int32 array: 113361us.
Time of processing int64 array: 224451us.
С предварительными ошибками: http://coliru.stacked-crooked.com/a/7e62b9c7ca19c128
g++ -O3 -Wall main.cpp && ./a.out
Time of processing int8 array: 6216us.
Time of processing int16 array: 12472us.
Time of processing int32 array: 24961us.
Time of processing int64 array: 49886us.
Время уменьшается примерно в 4 раза. Другими словами, ваш исходный тест измерял больше ядра, чем фактический код.
Проблема 2: Компилятор уничтожает контрольный показатель
Компилятор распознает ваш образец пишущих нулей и полностью заменяет все ваши циклы на вызовы memset()
. Таким образом, вы измеряете вызовы memset()
с разными размерами.
call std::chrono::_V2::system_clock::now()
xor esi, esi
mov edx, 67108864
mov edi, OFFSET FLAT:int8Array
mov r14, rax
call memset
call std::chrono::_V2::system_clock::now()
xor esi, esi
mov edx, 134217728
mov edi, OFFSET FLAT:int16Array
mov r13, rax
call memset
call std::chrono::_V2::system_clock::now()
xor esi, esi
mov edx, 268435456
mov edi, OFFSET FLAT:int32Array
mov r12, rax
call memset
call std::chrono::_V2::system_clock::now()
xor esi, esi
mov edx, 536870912
mov edi, OFFSET FLAT:int64Array
mov rbp, rax
call memset
call std::chrono::_V2::system_clock::now()
Оптимизация, выполняющая это, -ftree-loop-distribute-patterns
. Даже если вы отключите это, векторный инструмент даст вам аналогичный эффект.
С -O2
, векторизация и распознавание образов оба отключены. Итак, компилятор дает вам то, что вы пишете.
.L4:
mov BYTE PTR [rax], 0 ;; <<------ 1 byte at a time
add rax, 1
cmp rdx, rax
jne .L4
call std::chrono::_V2::system_clock::now()
mov rbp, rax
mov eax, OFFSET FLAT:int16Array
lea rdx, [rax+134217728]
.L5:
xor ecx, ecx
add rax, 2
mov WORD PTR [rax-2], cx ;; <<------ 2 bytes at a time
cmp rdx, rax
jne .L5
call std::chrono::_V2::system_clock::now()
mov r12, rax
mov eax, OFFSET FLAT:int32Array
lea rdx, [rax+268435456]
.L6:
mov DWORD PTR [rax], 0 ;; <<------ 4 bytes at a time
add rax, 4
cmp rax, rdx
jne .L6
call std::chrono::_V2::system_clock::now()
mov r13, rax
mov eax, OFFSET FLAT:int64Array
lea rdx, [rax+536870912]
.L7:
mov QWORD PTR [rax], 0 ;; <<------ 8 bytes at a time
add rax, 8
cmp rdx, rax
jne .L7
call std::chrono::_V2::system_clock::now()
С -O2
: http://coliru.stacked-crooked.com/a/edfdfaaf7ec2882e
g++ -O2 -Wall main.cpp && ./a.out
Time of processing int8 array: 28414us.
Time of processing int16 array: 22617us.
Time of processing int32 array: 32551us.
Time of processing int64 array: 56591us.
Теперь ясно, что размеры меньшего слова медленнее. Но вы ожидали бы, что время будет плоским, если все размеры слова будут одинаковой скоростью. И причина, по которой они не связаны, связана с пропускной способностью памяти.
Проблема 3: Пропускная способность памяти
Поскольку эталон (как написано) записывает только нули, он легко насыщает полосу пропускания памяти для ядра/системы. Таким образом, на тест попадает влияние того, сколько памяти было затронуто.
Чтобы исправить это, нам нужно сжать набор данных так, чтобы он входил в кеш. Чтобы компенсировать это, мы повторяем одни и те же данные несколько раз.
std::array<std::int8_t, 512> int8Array;
std::array<std::int16_t, 512> int16Array;
std::array<std::int32_t, 512> int32Array;
std::array<std::int64_t, 512> int64Array;
...
auto point1 = std::chrono::high_resolution_clock::now();
for (int c = 0; c < 64 * 1024; c++) for (auto &v : int8Array) v = 0;
auto point2 = std::chrono::high_resolution_clock::now();
for (int c = 0; c < 64 * 1024; c++) for (auto &v : int16Array) v = 0;
auto point3 = std::chrono::high_resolution_clock::now();
for (int c = 0; c < 64 * 1024; c++) for (auto &v : int32Array) v = 0;
auto point4 = std::chrono::high_resolution_clock::now();
for (int c = 0; c < 64 * 1024; c++) for (auto &v : int64Array) v = 0;
auto point5 = std::chrono::high_resolution_clock::now();
Теперь мы видим тайминги, которые намного более плоские для разных размеров слова:
http://coliru.stacked-crooked.com/a/f534f98f6d840c5c
g++ -O2 -Wall main.cpp && ./a.out
Time of processing int8 array: 20487us.
Time of processing int16 array: 21965us.
Time of processing int32 array: 32569us.
Time of processing int64 array: 26059us.
Причина, по которой она не полностью плоская, вероятно, связана с многочисленными другими факторами, связанными с оптимизацией компилятора. Возможно, вам придется прибегнуть к циклическому разворачиванию, чтобы приблизиться.