#c #cpu-cache
#c #cpu-cache
Вопрос:
Является ли работа с непрерывными буферами предпочтительной для кэширования процессора?
Я пытаюсь написать приложение, которое выполняет несколько операций с изображением (некоторые очень локальные, такие как сдвиг, производные, а некоторые между результатами, такими как вычитание). У меня есть большой буфер для результатов (каждое вычисление имеет форму изображения, поэтому я начинаю с выделения байтов формы X * изображения)
Что я должен сделать для максимизации обращений к кэшу процессора?
Комментарии:
1. Является ли работа с непрерывными буферами предпочтительной для кэширования процессора? ДА. Кэш — это самая быстрая память, которая у вас есть. Если вы можете оставаться в кэше, вы будете работать так быстро, как только сможете, по крайней мере, с точки зрения чтения-записи.
2. Вы также можете рассмотреть возможность использования графического процессора.
3. Не только кэширование, но и предварительная выборка. Наличие предсказуемого и понятного шаблона доступа к данным помогает процессору выполнять предварительную выборку ваших данных до того, как они вам понадобятся. Это само по себе может сэкономить тысячи циклов ожидания оперативной памяти, когда у процессора могли быть готовые данные для вас.
4. если операция с изображением сложнее, чем просто вычитание, например, сглаживание / фильтрация, вам следует рассмотреть многопоточность
5. Попробуйте разбить изображение на маленькие квадраты. Выполните как можно больше функций в этом квадрате. Затем перейдите к следующему квадрату. Может быть, лучше по строкам; только сравнительный анализ покажет. Другими словами, делайте как можно больше, пока данные находятся в кэше, предотвращая перезагрузку кэша.
Ответ №1:
Конечно, работа с непрерывным массивом с большей вероятностью приведет к кешированию.
Возможно, вы захотите упорядочить свои данные непрерывным образом:
например:
#include <iostream>
#include <cstdint>
const int SIZE = 3;
int main(){
uint8_t buffer_2d[SIZE][SIZE];
uint8_t* buffer_1d = reinterpret_cast<uint8_t*>(buffer_2d); // or just do uint8_t buffer_1d[SIZE*SIZE];
const auto base = amp;(buffer_2d[0][0]);
for (int y=0;y<SIZE; y){
for (int x=0;x<SIZE; x){
std::cout << "x: " << x << " y: " << y << " offset for [x][y]: " << amp;(buffer_2d[x][y]) - base << " offset for [y][x]: " << amp;(buffer_2d[y][x]) - amp;(buffer_2d[0][0]) << " offset for [y*SIZE x]: " << amp;(buffer_1d[y*SIZE x]) - base << std::endl;
}
}
return 0;
}
Обработка массива как естественного человеческого [x][y]
массива была бы неэффективной, поскольку данные не выровнены таким образом, эффективным подходом было бы использовать [y][x]
или работать с массивом как с массивом одного измерения и обрабатывать индекс как y*LINE_SIZE x
.
Вот результат этого теста, показывающий именно это:
x: 0 y: 0 offset for [x][y]: 0 offset for [y][x]: 0 offset for [y*SIZE x]: 0
x: 1 y: 0 offset for [x][y]: 3 offset for [y][x]: 1 offset for [y*SIZE x]: 1
x: 2 y: 0 offset for [x][y]: 6 offset for [y][x]: 2 offset for [y*SIZE x]: 2
x: 0 y: 1 offset for [x][y]: 1 offset for [y][x]: 3 offset for [y*SIZE x]: 3
x: 1 y: 1 offset for [x][y]: 4 offset for [y][x]: 4 offset for [y*SIZE x]: 4
x: 2 y: 1 offset for [x][y]: 7 offset for [y][x]: 5 offset for [y*SIZE x]: 5
x: 0 y: 2 offset for [x][y]: 2 offset for [y][x]: 6 offset for [y*SIZE x]: 6
x: 1 y: 2 offset for [x][y]: 5 offset for [y][x]: 7 offset for [y*SIZE x]: 7
x: 2 y: 2 offset for [x][y]: 8 offset for [y][x]: 8 offset for [y*SIZE x]: 8
последние два результата используют точно такую же семантику, первый позволит компилятору выполнить вычисления, но производительность должна быть одинаковой.
Кроме того, как только ваши данные упорядочены правильно, в зависимости от того, что вы делаете с данными, вы можете захотеть использовать OpenCL или что-то еще и использовать GPU или SIMD, что может привести к значительному повышению производительности, если это может быть выражено в коде SIMD (Single Instruction Multiple Data).
Ответ №2:
#ifdef _MSC_VER
_declspec(align(64)) unsigned char block_hashfp;
#else
__attribute__((aligned(64))) unsigned char block_hashfp;
#endif
попадет в кэш l2
Комментарии:
1. Пожалуйста, поясните, как выравнивание памяти до 64-разрядной границы гарантирует, что память находится в кэше.
2. 64 байта. e-maxx.ru/bookz/files/intel_optimization.pdf
3. Обратите внимание, что теперь есть стандартный способ поиска выравниваний
std::hardware_destructive_interference_size
4. @Mgetz в их примере выводится 64. но почему они говорят о
level 1
cache idk. 64 будет работать в любом случае5. @АлексейНеудачин потому что для прямой целевой цели этой функциональности L1 будет целью. Потому что именно там произойдет первоначальный сбой false sharing. Тот факт, что он совпадает с размером строки кэша, — это просто приятное удобство того, как работает false sharing.