. Как влияет кеш на многопоточные приложения
Как влияет кеш на многопоточные приложения

Как влияет кеш на многопоточные приложения

Так сложилось, узким местом во многих компьютерах является интерфейс оперативная память — процессор. Связано это со значительным временем формирования запроса в память и с тем что частота работы памяти ниже частоты процессора. В общем случае на время получения данных из памяти исполнение программы останавливается. Для улучшения ситуации используется специализированная высокоскоростная память – кеш.

При наличии кеша, если процессору требуется обратиться к определенному байту в оперативной памяти, то сначала выбирается область памяти содержащая этот байт (размер области зависит от процессора) в кеш, а затем происходит считывание данных из кеша процессором. Если процессор записывает данные в память, то они сначала попадают в кеш, а затем в оперативную память.

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

У меня возник вопрос насколько все может быть плохо. Кроме того, интересно было посмотреть как влияет длина данных.

Эксперимент. Для проверки были написаны 2 многопоточных программы на С++ для двух случаев пересечения рабочих областей:

  • Области не пересекаются и находятся в стеке каждой из нитей;
  • Рабочая область для нитей общая, представляет собой массив из 1, 4 и 8 байтных слов, общей длиной 1024 байта, каждая из нитей модифицирует свои слова (например только четные или только нечетные).

Рабочих нитей – 2, каждая, меняя свои слова, проходит весь массив и, достигнув границы, возвращается к началу и так далее. Каждая из нитей делает по 100 000 000 изменений.

UPD Спасибо mikhanoid за выявленную опечатку в названии CPU. Для тестирования использовалась рабочая станция HP xw4600, CPU Core 2 Quad (Q9300), OS Linux Slamd64 12.1, ядро 2.6.24.5, компилятор gcc 4.2.3. В итоге получалась 64-х битная программа, следовательно любая арифметическая операция для слов длиной 1,4 и 8 байт выполнялась за 1 такт.

Время исполнения измерялось командой time(1), и усреднялось по 5-и экспериментам.

Сначала была написана программа, которая просто инкрементирует слово и кладет его обратно, но оказалось что в случае массива на стеке оптимизация -O2 творит что-то непотребное. Поэтому была написана вторая программа, которая делает с данными что-то более сложное.

Результаты. UPD: Thnx за замечание mt_ и Google Charts за api графиков. Результат в графической форме. Желающие посмотреть цифры, могут промотать дальше.

По оси Y реальное время исполнения программы в секундах. По оси X разрядность слова (парами).

Программа 2 + оптимизация -O2.

Выводы Можно точно сказать, что в случае заведомо не пересекающихся областей работает быстрее, причем значительно. Оптимизация как-то влияет, но тенденция сохраняется.

А вот зависимость от длины слова как-то мало заметна.

UPD Попытка перенести в блог Совершенный код, мне кажется что это самое правильное место.

Результаты в табличной форме. Результаты представлены в секундах.

Программа 1 (общая память). Длина слова 64 32 8 Реальное время 1,297 1,532 0,846 Время польз. режима 2,461 3,049 1,664

Программа 1 (стек) Длина слова 64 32 8 Реальное время 0,453 0,431 0,441 Время польз. режима 0,851 0,842 0,866

Программа 2 (общая память). Длина слова 64 32 8 Реальное время 9,191 9,033 8,921 Время польз. режима 18,365 18,039 17,824

Программа 2 (стек) Длина слова 64 32 8 Реальное время с 8,432 8,412 8,808 Время польз. Режима с 16,843 16,796 17,548

Программа 2 + оптимизация -O2 (общая память). Длина слова 64 32 8 Реальное время 4,247 4,380 3,473 Время польз. режима 8,415 8,960 6,781

Программа 2 + оптимизация -O2 (стек) Длина слова 64 32 8 Реальное время 3,282 3,718 3,308 Время польз. режима 6,550 7,384 5,565

Исходный код.

Программа 1.

#include <stdio.h> #include <stdlib.h>

#define TBYTES 1024 // #define TOTAL 2 // определяет тип слова u_int8_t u_int32_t u_int64_t #define MTYPE u_int8_t // #define MAXAR TBYTES/ sizeof (MTYPE) #define DOIT 100000000

pthread_mutex_t mux = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; int workers = 2;

// Определяет используется ли общая память //#define SHARED 1

void * runner( void * args) // pthread_mutex_lock(&mux); -- workers; pthread_mutex_unlock(&mux); pthread_cond_broadcast(&cond); // return 0; >;

int main() ; // * This source code was highlighted with Source Code Highlighter .

Программа 2.

#include <stdio.h> #include <stdlib.h>

#define TBYTES 1024 // #define TOTAL 2 // определяет тип слова u_int8_t u_int32_t u_int64_t #define MTYPE u_int64_t // #define MAXAR TBYTES/ sizeof (MTYPE) //#define DOIT 100000000 * (sizeof(u_int64_t) / sizeof(MTYPE)) #define DOIT 100000000

pthread_mutex_t mux = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; int workers = 2;

template<typename RTYPE, int TLEN> RTYPE scramble(u_int16_t *seed, RTYPE a) 1) &0x1); mask = mask & 0x1; *seed = (mask << 15) | (*seed >> 1); // a = a ^ ((RTYPE)mask << d); > // return a; >;

// Определяет используется ли общая память //#define SHARED 1

void * runner( void * args) (&seed, data); #else data = array1[index]; array1[index] = scramble<MTYPE, 8* sizeof (MTYPE)>(&seed, data); #endif // index += TOTAL; > // pthread_mutex_lock(&mux); -- workers; pthread_mutex_unlock(&mux); pthread_cond_broadcast(&cond); // return 0; >;

int main() ; // * This source code was highlighted with Source Code Highlighter .

📎📎📎📎📎📎📎📎📎📎