В общем смысле, как я вижу по твоему коду, ты вляпался в True Sharing, попутно обмазавшись Cache Misses и окончательно убив свою производительность с помощью неоправданно огромного размера клеток.
8Б на клетку, состояние которой может поместиться в 1Б, это действительно огромный размер.
enum CellState : uint8_t
уменьшит размер состояния с 4Б до 1Б. А еще этот тип стоит переименовать, т.к. это не
CellState
, а что-то относящееся к поведению клетки. А вот
CellState
будет выглядеть так:
// Renamed from `CellState`.
enum CellBehavior : uint8_t
{
Empty,
Alive,
};
struct CellState final
{
CellBehavior current_behavior : 4;
CellBehavior next_behavior : 4;
};
Это позволяет уменьшить размер клетки до 1 байта.
Данные оперативной памяти процессор подтягивает к себе во
внутренний кэш.
Кэшей у процессора много и все они связаны. Кэш процессора
поделен на линии, работа с которыми синхронизируется между ядрами процессора. Вот именно тут появляется два термина: False cacheline sharing и True cacheline sharing. Если "False", то обрабатываемые разными ядрами данные разделены в разные кэш-линии. Когда "True" - требуемые разным ядрам данные находятся в одной кэш-линии и привет синхронизация. А это ой как медленно.
В каждом процессоре сегодня сидит гадалка, которая предсказывает какие тебе надо подтянуть данные из RAM в CPU Cache. Выборка из RAM - это довольно долгая процедура, поэтому нужна гадалка чтобы предсказать что судьбой твоего алгоритма предначертано выбрать на следующем этапе. Бывает что гадалка ошибается и тогда твой лагоритм встает в синхронизацию до завершения нужной выборки из памяти. А это - еще медленнее чем синхронизация по кэш-линиям. Это называется промахом по кэшу - cache miss.
К счастью, это не гадалка виновата в своей ошибке, а ты просто неправильно написал лагоритм. Вот чтобы из лагоритма сделать алгоритм, следует
озаботиться чтобы он был более лоялен к гадалке и кэшу процессора.
Докину еще немного полезной информации.
Сходи к
Адаму Мартину и к
Unity, посмотри на парадигму
ES/ESP/ECS. Изучи
DOD. Попробуй реорганизацию из твоего текущего потока сущностей с полями в потоки полей сущностей. Переделай батчинг обработки клеток так, чтобы данные не синхронизировались между ядрами процессора.
Возможно тебе еще поможет понимание подхода
Out of line, т.к. там хорошо объясняется почему очень большие объекты при их поточной обработке - это не очень дружественно кэшу процессора.
Еще сюда можно добавить информацию о
автоматической векторизации. Это позволит задействовать SIMD инструкции для твоего кода. DOD очень элегантно ложится для обработки твоих клеток SIMD командами.
Я тут крайне сумбурно накидал, только чтобы дать тебе направления. Кое-чего я даже не написал, но ты обязательно зацепишь все неописанное когда будешь изучать то, что я описал. Думаю, ты уже видишь, в какой объем выльется весь этот материал, если писать его в удобном понятном формате и раскрывая каждую тему.