Это почти наверняка вопрос когерентности кэша. В GLSL int[WIDTH][HEIGHT]
- это массив HEIGHT
1D массивов WIDTH
int
с. Это главный ряд. Поэтому, если вы выбираете previousBoard.data[0][0]
, вы выбираете строку кэша (при условии 32 байта), которая может включать следующие 7 элементов первой строки и none второй строки.
Ваш шейдер сам выполняется в 2D-модели с 32x32 вызовами в рабочей группе. Если графический процессор выполняет вызовы первой строки (от 0,0 до 31,0) в одно и то же время, ему потребуется только 4 фактических выборки памяти. Теперь, конечно, чтобы выполнить алгоритм для всех этих записей, вам понадобится также предыдущая строка и следующая строка, а также строка кэша для адресов памяти справа.
Итак, вам нужно 15 выборок памяти. Это может звучать как много.
Но давайте рассмотрим случай, когда графический процессор выполняет вызовы первого столбца *1015*: от 0,0 до 0,31. Ну, сколько вам нужно? Вам нужно 33 (+1 для строки ниже дна), более чем вдвое больше. Помните: строки кэша являются основными строками, а не ориентированы на столбцы.
И, конечно, вам понадобится столько же записей в кэш-памяти.
При этом порядок вызовов в первом столбце должен быть в состоянии повысить производительность, поскольку вызовы второго столбца должны получать те же строки кэша, что и первый. Но это предполагает, что реализация будет выполнять вызовы второго столбца одновременно. Если вместо этого он решит заполнить свои исполнительные блоки большим количеством рабочих групп (то есть он выполняет столбец 0, столбец 32, столбец 64, столбец 96 и т. Д.), То у вас также может не быть кеша.
В отличие от этого, порядок в первой строке сохраняет разумную когерентность кэша независимо от порядка, в котором он выполняется.
Вы ничего не можете сделать, чтобы изменить порядок обработки ваших вызовов графическим процессором. Поэтому вместо этого вы должны постараться сделать так, чтобы ваш алгоритм как можно меньше заботился об этом порядке.
Во-первых, поскольку нет никакой зависимости между вашими вызовами в рабочей группе, вы не должны делать ваш local_size
двухмерным. Вы можете поиграть с точными числами, чтобы найти правильное значение для оборудования, но 16x1 или 32x1, вероятно, будут работать. Там нет никакой гарантии в отношении порядка вызова, но элементы в рабочей группе, которые вписываются в волновой фронт, будут иметь тенденцию выполняться вместе. Так что это побудит его работать по-крупному, выполняя 0,0; 1,0; и т. д. одновременно.
Во-вторых, Пожалуйста, уменьшите объем используемого пространства. Игра жизни имеет ровно два состояния для клетки. Но вы используете тридцать два бита для хранения этих двух состояний. Даже если вы хотите избежать боли при выполнении серьезных битовых манипуляций, вы можете, по крайней мере, каждый байт uint
быть отдельной ячейкой. Извлечение N-го байта из uint
- довольно тривиальный процесс.
Сложная задача будет записывать такие данные, поскольку у вас разные вызовы для записи отдельных данных. Но если мы предположим, что вы очистили память до нуля перед запуском, вы можете использовать atomicOr
для записи значения.
В-третьих, используйте мерзость на ваших данных. То есть вместо того, чтобы хранить его как строки и столбцы, храните его в блоках. Основная проблема, с которой вы столкнулись, заключается в том, что ваш кэш смещен в сторону первой координаты, а ваш графический процессор работает со смещением ко второй.
Неподготовленные данные помещают (0, 0) в байт 0, (1, 0) в байт 4 и (0, 1) в байт (4 *WIDTH
). С помощью swizzling вы хотите поместить четыре байта 0,0; 1,0; 0,1 и 1,1 все в одной строке кэша. То есть (0, 1) находится в байте 8, а (1, 1) в байте 12. Таким образом, если вы выбираете (1, 1), вы гарантированно получите все 4 значения в одной строке кэша .
Для максимальной производительности вы можете поиграть с образцами изгиба.
И, кроме того, вы можете даже взмахнуть своим gl_InvocationID
.Вместо того чтобы полагаться на 2D-характер вашей отправки для получения исходной позиции для вызова, вы можете сделать вашу отправку одномерной и вычислить xy
позицию вызова с помощью матрицы Swizzle.Таким образом, вызов 0 будет (0, 0), вызов 1 будет (1, 0), вызов 2 будет (0, 1), вызов 3 будет (1, 1) и т. Д.
Если вы включите работу, чтобы получить наиболее оптимальное хранилище данных с быстрым движением, то каждая строка кэша может представлять блок данных 8x8.Это означает, что любой непрерывно выполняющейся группе вызовов потребуется только максимум 4 строки данных кэша (в углу из 4 блоков).Кроме того, это помогает решить проблему записи, поскольку вы можете строить данные с помощью атомарных операций для shared
переменных и просто записывать значения в конце.Вы упорядочиваете все так, чтобы никаким двум вызовам из отдельных рабочих групп не приходилось записывать одно и то же значение.
Это сделало бы все в значительной степени независимым от выполнения GPU.