Подтвердить что ты не робот

Почему массивы разных целых размеров имеют разную производительность?

У меня есть следующая проблема:

Время записи до std::array для int8, int16, int32 и int64 удваивается при каждом увеличении размера. Я понимаю такое поведение для 8-битного процессора, но не 32/64-битного.

Почему для 32-разрядной системы требуется в 4 раза больше времени для сохранения 32-битных значений, чем для сохранения 8-битных значений?

Вот мой тестовый код:

#include <iostream>
#include <array>
#include <chrono>

std::array<std::int8_t, 64 * 1024 * 1024> int8Array;
std::array<std::int16_t, 64 * 1024 * 1024> int16Array;
std::array<std::int32_t, 64 * 1024 * 1024> int32Array;
std::array<std::int64_t, 64 * 1024 * 1024> int64Array;

void PutZero()
{
    auto point1 = std::chrono::high_resolution_clock::now();
    for (auto &v : int8Array) v = 0;
    auto point2 = std::chrono::high_resolution_clock::now();
    for (auto &v : int16Array) v = 0;
    auto point3 = std::chrono::high_resolution_clock::now();
    for (auto &v : int32Array) v = 0;
    auto point4 = std::chrono::high_resolution_clock::now();
    for (auto &v : int64Array) v = 0;
    auto point5 = std::chrono::high_resolution_clock::now();
    std::cout << "Time of processing int8 array:\t" << (std::chrono::duration_cast<std::chrono::microseconds>(point2 - point1)).count() << "us." << std::endl;
    std::cout << "Time of processing int16 array:\t" << (std::chrono::duration_cast<std::chrono::microseconds>(point3 - point2)).count() << "us." << std::endl;
    std::cout << "Time of processing int32 array:\t" << (std::chrono::duration_cast<std::chrono::microseconds>(point4 - point3)).count() << "us." << std::endl;
    std::cout << "Time of processing int64 array:\t" << (std::chrono::duration_cast<std::chrono::microseconds>(point5 - point4)).count() << "us." << std::endl;
}

int main()
{
    PutZero();
    std::cout << std::endl << "Press enter to exit" << std::endl;
    std::cin.get();
    return 0;
}

Я скомпилирую его под linux с помощью: g++ -o array_issue_1 main.cpp -O3 -std=c++14

и мои результаты следующие:

Time of processing int8 array:  9922us.   
Time of processing int16 array: 37717us.   
Time of processing int32 array: 76064us.   
Time of processing int64 array: 146803us.   

Если я скомпилирован с -O2, то результаты в 5 раз хуже для int8!

Вы также можете скомпилировать этот источник в Windows. Вы получите аналогичную связь между результатами.

Обновление # 1

Когда я компилирую с -O2, мои результаты следующие:

Time of processing int8 array:  60182us.  
Time of processing int16 array: 77807us.  
Time of processing int32 array: 114204us.  
Time of processing int64 array: 186664us.  

Я не анализировал выход ассемблера. Главное, что я хотел бы написать эффективный код на С++ и что-то подобное, что такие вещи, как std::array, могут быть сложными с точки зрения производительности и как-то контр-интуитивно понятными.

4b9b3361

Ответ 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.

Причина, по которой она не полностью плоская, вероятно, связана с многочисленными другими факторами, связанными с оптимизацией компилятора. Возможно, вам придется прибегнуть к циклическому разворачиванию, чтобы приблизиться.