Использование кэша процессора для повышения производительности в C

#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.