Как разбить массив на блоки
у меня есть массив, который представляет точки в кубоиде. Это одномерный массив, который использует следующую функцию индексирования для реализации 3 измерений:
int getCellIndex(int ix, int iy, int iz) {
return ix + (iy * numCellsX) + (iz * numCellsX * numCellsY);
}
количество ячеек в домене:
numCells = (numX + 2) * (numY + 2) * (numZ + 2)
где numX/numY/numZ-количество ячеек в направлении X/Y / Z. +2 в каждом направлении-это создание ячеек заполнения вокруг внешней части домена. Количество ячеек в каждом направлении определяется по формуле:
numX = 5 * numY
numZ = numY/2
numY = userInput
для каждая ячейка, я хочу вычислить новое значение для этой ячейки на основе ее значения соседей (т. е. трафарета), где соседи находятся выше, ниже, слева, справа, спереди и сзади. Однако я хочу сделать этот расчет только для ячеек, которые не плохи. У меня есть логический массив, который отслеживает, если ячейка плохая. Вот как в настоящее время выглядит вычисление:
for(int z = 1; z < numZ+1; z++) {
for(int y = 1; y < numY+1; y++) {
for(int x = 1; x < numX+1; x++) {
if(!isBadCell[ getCellIndex(x,y,z) ] {
// Do stencil Computation
}
}
}
}
это не большой производительности мудрый. Я хочу иметь возможность векторизовать цикл для повышения производительности, однако я не могу из-за if оператор. Я знаю, если ячейки плохи заранее, и это не меняется на протяжении всего вычисления. Я хотел бы разделить домен на блоки, предпочтительно блоки 4x4x4, чтобы я мог вычислить априори на блок, если он содержит плохие ячейки, и если это так, как обычно, или если нет, используйте оптимизированную функцию, которая может воспользоваться векторизацией, например
for(block : blocks) {
if(isBadBlock[block]) {
slowProcessBlock(block) // As above
} else {
fastVectorizedProcessBlock(block)
}
}
примечание: не требуется, чтобы блоки физически существовали, т. е. это может быть достигнуто путем изменения функция индексирования и использование различных индексов для цикла над массивом. Я открыт для всего, что работает лучше.
функция fastVectorizedProcessBlock() будет выглядеть аналогично функции slowProcessBlock (), но с оператором if remove (поскольку мы знаем, что он не содержит плохих ячеек) и прагмой векторизации.
как я могу разделить свой домен на блоки, чтобы я мог это сделать? Это кажется сложным, потому что a) количество ячеек в каждом направлении не равно, b) нам нужно учитывать ячейки заполнения, поскольку мы никогда не должны пытаться вычислить их значение, поскольку это приведет к доступу к памяти, который выходит за рамки.
как я могу обрабатывать блоки, которые не содержат плохих ячеек без использования оператора if?
EDIT:
это идея, которую я первоначально имел:
for(int i = 0; i < numBlocks; i++) { // use blocks of 4x4x4 = 64
if(!isBadBlock[i]) {
// vectorization pragma here
for(int z = 0; z < 4; z++) {
for(int y = 0; y < 4; y++) {
for(int x = 0; x < 4; x++) {
// calculate stencil using getCellIndex(x,y,z)*i
}
}
}
} else {
for(int z = 0; z < 4; z++) {
for(int y = 0; y < 4; y++) {
for(int x = 0; x < 4; x++) {
if(!isBadCell[i*getCellIndex(x,y,z)]) {
// calculate stencil using getCellIndex(x,y,z)*i
}
}
}
}
}
ячейки теперь будут храниться в блоках, т. е. все ячейки в первом блоке 4x4x4 будут храниться в pos 0-63, затем все клетки во втором блоке будут храниться в POS 64-127 и т. д.
однако я не думаю, что будет работать, если значения numX/numY/numZ не являются добрыми. Например, что делать, если numY = 2, numZ = 1 и numX = 10? Для петель ожидалось бы, что направление z будет иметь глубину не менее 4 ячеек. Есть ли хороший способ пройти через это?
UPDATE 2-Вот как выглядит вычисление трафарета:
if ( isBadCell[ getCellIndex(x,y,z) ] ) {
double temp = someOtherArray[ getCellIndex(x,y,z) ] +
1.0/CONSTANT/CONSTANT*
(
- 1.0 * cells[ getCellIndex(x-1,y,z) ]
- 1.0 * cells[ getCellIndex(x+1,y,z) ]
- 1.0 * cells[ getCellIndex(x,y-1,z) ]
- 1.0 * cells[ getCellIndex(x,y+1,z) ]
- 1.0 * cells[ getCellIndex(x,y,z-1) ]
- 1.0 * cells[ getCellIndex(x,y,z+1) ]
+ 6.0 * cells[ getCellIndex(x,y,z) ]
);
globalTemp += temp * temp;
cells[ getCellIndex(x,y,z) ] += -omega * temp / 6.0 * CONSTANT * CONSTANT;
}
4 ответов
где getCellIndex()
восстановить значения numCellX
и numCellY
? Было бы лучше передать их в качестве аргументов, а не полагаться на глобальные переменные, и сделать эту функцию static inline
для оптимизации компилятора.
static line int getCellIndex(int ix, int iy, int iz, int numCellsX, numCellsY) {
return ix + (iy * numCellsX) + (iz * numCellsX * numCellsY);
}
for (int z = 1; z <= numZ; z++) {
for (int y = 1; y <= numY; y++) {
for (int x = 1; x <= numX; x++) {
if (!isBadCell[getCellIndex(x, y, z, numX + 2, numY + 2)] {
// Do stencil Computation
}
}
}
}
вы также можете удалить все умножения с некоторыми локальными переменными:
int index = (numY + 2) * (numX + 2); // skip top padding plane
for (int z = 1; z <= numZ; z++) {
index += numX + 2; // skip first padding row
for (int y = 1; y <= numY; y++) {
index += 1; // skip first padding col
for (int x = 1; x <= numX; x++, index++) {
if (!isBadCell[index] {
// Do stencil Computation
}
}
index += 1; // skip last padding col
}
index += numX + 2; // skip last padding row
}
являются ли эти направления обещающими или нет, во многом зависит от фактических вычислений, выполненных для получения значения трафарета. Вы должны и это тоже.
если вы можете изменить формат логического массива для плохих ячеек, было бы полезно заполнить строки кратным 8 и использовать горизонтальное заполнение 8 столбцов для улучшения выравнивания. Создание логического массива массив битов позволяет проверить 8, 16, 32 или даже 64 ячейки одновременно с помощью одного теста.
вы можете отрегулировать указатель массива с 0 координаты.
вот как это было работа:
int numCellsX = 8 + ((numX + 7) & ~7) + 8;
int numCellsY = 1 + numY + 1;
int numCellsXY = numCellsX * numCellsY;
// adjusted array_pointer
array_pointer = allocated_pointer + 8 + numCellsX + numCellsXY;
// assuming the isBadCell array is 0 based too.
for (int z = 0, indexZ = 0; z < numZ; z++, indexZ += numCellsXY) {
for (int y = 0, indexY = indexZ; y < numY; y++, indexY += numCellsX) {
for (int x = 0, index = indexY; x <= numX - 8; x += 8, index += 8) {
int mask = isBadCell[index >> 3];
if (mask == 0) {
// let the compiler unroll computation for 8 pixels with
for (int i = 0; i < 8; i++) {
// compute stencil value for x+i,y,z at index+i
}
} else {
for (int i = 0; i < 8; i++, mask >>= 1) {
if (!(mask & 1)) {
// compute stencil value for x+i,y,z at index+i
}
}
}
}
int mask = isBadCell[index >> 3];
for (; x < numX; x++, index++, mask >>= 1) {
if (!(mask & 1)) {
// compute stencil value for x,y,z at index
}
}
}
}
EDIT:
функция stencil использует слишком много вызовов для getCellIndex. Вот как его оптимизировать, используя значение индекса, вычисленное в приведенном выше коде:
// index is the offset of cell x,y,z
// numCellsX, numCellsY are the dimensions of the plane
// numCellsXY is the offset between planes: numCellsX * numCellsY
if (isBadCell[index]) {
double temp = someOtherArray[index] +
1.0 / CONSTANT / CONSTANT *
( - 1.0 * cells[index - 1]
- 1.0 * cells[index + 1]
- 1.0 * cells[index - numCellsX]
- 1.0 * cells[index + numCellsX]
- 1.0 * cells[index - numCellsXY]
- 1.0 * cells[index + numCellsXY]
+ 6.0 * cells[index]
);
cells[index] += -omega * temp / 6.0 * CONSTANT * CONSTANT;
globalTemp += temp * temp;
}
препроцесса &cells[index]
как указатель может улучшить код, но компиляция должна быть в состоянии обнаружить это общее подвыражение и уже генерировать эффективный код.
EDIT2:
вот плиточный подход: вы можете добавить отсутствующие аргументы, большинство размеров считаются глобальными, но вы, вероятно, должны передать указатель на структуру контекста со всеми этими значениями. Он использует isBadTile[]
и isGoodTile[]
: массивы булевых сообщений, если данная плитка имеет все ячейки плохо и все ячейки хорошо соответственно.
void handle_tile(int x, int y, int z, int nx, int ny, int nz) {
int index0 = x + y * numCellsX + z * numCellsXY;
// skipping a tile with all cells bad.
if (isBadTile[index0] && nx == 4 && ny == 4 && nz == 4)
return;
// handling a 4x4x4 tile with all cells OK.
if (isGoodTile[index0] && nx == 4 && ny == 4 && nz == 4) {
for (int iz = 0; iz < 4; iz++) {
for (int iy = 0; iy < 4; iy++) {
for (int ix = 0; ix < 4; ix++) {
int index = index0 + ix + iy * numCellsX + iz + numCellsXY;
// Do stencil computation using `index`
}
}
}
} else {
for (int iz = 0; iz < nz; iz++) {
for (int iy = 0; iy < ny; iy++) {
for (int ix = 0; ix < nx; ix++) {
int index = index0 + ix + iy * numCellsX + iz + numCellsXY;
if (!isBadCell[index] {
// Do stencil computation using `index`
}
}
}
}
}
void handle_cells() {
int x, y, z;
for (z = 1; z <= numZ; z += 4) {
int nz = min(numZ + 1 - z, 4);
for (y = 1; y <= numY; y += 4) {
int ny = min(numY + 1 - y, 4);
for (x = 1; x <= numX; x += 4) {
int nx = min(numX + 1 - x, 4);
handle_tile(x, y, z, nx, ny, nz);
}
}
}
}
вот функция для вычисления isGoodTile[]
массив. Единственные правильно вычисленные смещения соответствуют значениям X кратных 4 + 1, y и z меньше 3 от их максимума ценности.
эта реализация является неоптимальной, поскольку может быть вычислено меньше элементов. Неполные пограничные плитки (менее 4 от края) могут быть помечены как не хорошие, чтобы пропустить хороший случай с одним случаем. Тест на плохие плитки может работать для этих краевых плиток, если isBadTile
массив был правильно вычислен для краевых плиток, что в настоящее время не так.
void computeGoodTiles() {
int start = 1 + numCellsX + numCellsXY;
int stop = numCellsXY * numCellsZ - 1 - numCellsX - numCellsXY;
memset(isGoodTile, 0, sizeof(*isGoodTile) * numCellsXY * numCellsZ);
for (int i = start; i < stop; i += 4) {
isGoodTile[i] = (isBadCell[i + 0] | isBadCell[i + 1] |
isBadCell[i + 2] | isBadCell[i + 3]) ^ 1;
}
for (int i = start; i < stop - 3 * numCellsX; i += 4) {
isGoodTile[i] = isGoodTile[i + 0 * numCellsX] &
isGoodTile[i + 1 * numCellsX] &
isGoodTile[i + 2 * numCellsX] &
isGoodTile[i + 3 * numCellsX];
}
for (int i = start; i < stop - 3 * numCellsXY; i += 4) {
isGoodTile[i] = isGoodTile[i + 0 * numCellsXY] &
isGoodTile[i + 1 * numCellsXY] &
isGoodTile[i + 2 * numCellsXY] &
isGoodTile[i + 3 * numCellsXY];
}
}
void computeBadTiles() {
int start = 1 + numCellsX + numCellsXY;
int stop = numCellsXY * numCellsZ - 1 - numCellsX - numCellsXY;
memset(isBadTile, 0, sizeof(*isBadTile) * numCellsXY * numCellsZ);
for (int i = start; i < stop; i += 4) {
isBadTile[i] = isBadCell[i + 0] & isBadCell[i + 1] &
isBadCell[i + 2] & isBadCell[i + 3];
}
for (int i = start; i < stop - 3 * numCellsX; i += 4) {
isBadTile[i] = isBadTile[i + 0 * numCellsX] &
isBadTile[i + 1 * numCellsX] &
isBadTile[i + 2 * numCellsX] &
isBadTile[i + 3 * numCellsX];
}
for (int i = start; i < stop - 3 * numCellsXY; i += 4) {
isBadTile[i] = isBadTile[i + 0 * numCellsXY] &
isBadTile[i + 1 * numCellsXY] &
isBadTile[i + 2 * numCellsXY] &
isBadTile[i + 3 * numCellsXY];
}
}
хотя OP требует подхода с использованием блокировки, я бы предложил против этого.
вы видите, что каждая последовательная последовательность ячеек (1D ячеек вдоль оси X) уже является таким блоком. вместо того, чтобы сделать проблему проще, блокировка просто заменяет исходную проблему меньшими копиями фиксированного размера, повторяемыми снова и снова.
проще говоря, блокировка не помогает вообще с реальной проблемой. Это не должно быть реквизит особенность решения вообще.
вместо этого я бы предложил вообще избегать корневой проблемы-просто по-другому.
вы видите, вместо того, чтобы иметь флаг "плохой ячейки" для каждой ячейки, которую вам нужно проверить (один раз для каждой ячейки, не меньше), вы можете сохранить (отсортированный) список индексов плохих ячеек. Затем вы можете обработать весь набор данных сразу, а затем исправить цикл над ячейками, перечисленными в индексе плохих ячеек список.
Также обратите внимание, что если вы работаете на скопировать из значений ячеек порядок, в котором вы вычисляете новые значения ячеек, повлияет на результат. Это почти наверняка не то, чего вы хотите.
Итак, вот мое предложение:
#include <stdlib.h>
#include <errno.h>
typedef struct {
/* Core cells in the state, excludes border cells */
size_t xsize;
size_t ysize;
size_t zsize;
/* Index calculation: x + y * ystride + z * zstride */
/* x is always linear in memory; xstride = 1 */
size_t ystride; /* = xsize + 2 */
size_t zstride; /* = ystride * (ysize + 2) */
/* Cell data, points to cell (0,0,0) */
double *current;
double *previous;
/* Bad cells */
size_t fixup_cells; /* Number of bad cells */
size_t *fixup_index; /* Array of bad cells' indexes */
/* Dynamically allocated memory */
void *mem[3];
} lattice;
void lattice_free(lattice *const ref)
{
if (ref) {
/* Free dynamically allocated memory, */
free(ref->mem[0]);
free(ref->mem[1]);
free(ref->mem[2]);
/* then initialize/poison the contents. */
ref->xsize = 0;
ref->ysize = 0;
ref->zsize = 0;
ref->ystride = 0;
ref->zstride = 0;
ref->previous = NULL;
ref->current = NULL;
ref->fixup_cells = 0;
ref->fixup_index = NULL;
ref->mem[0] = NULL;
ref->mem[1] = NULL;
ref->mem[2] = NULL;
}
}
int lattice_init(lattice *const ref, const size_t xsize, const size_t ysize, const size_t zsize)
{
const size_t xtotal = xsize + 2;
const size_t ytotal = ysize + 2;
const size_t ztotal = zsize + 2;
const size_t ntotal = xtotal * ytotal * ztotal;
const size_t double_bytes = ntotal * sizeof (double);
const size_t size_bytes = xsize * ysize * zsize * sizeof (size_t);
/* NULL reference to the variable to initialize? */
if (!ref)
return EINVAL;
/* Initialize/poison the lattice variable. */
ref->xsize = 0;
ref->ysize = 0;
ref->zsize = 0;
ref->ystride = 0;
ref->zstride = 0;
ref->previous = NULL;
ref->current = NULL;
ref->fixup_cells = 0;
ref->fixup_index = NULL;
ref->mem[0] = NULL;
ref->mem[1] = NULL;
ref->mem[2] = NULL;
/* Verify size is nonzero */
if (xsize < 1 || ysize < 1 || zsize < 1)
return EINVAL;
/* Verify size is not too large */
if (xtotal <= xsize || ytotal <= ysize || ztotal <= zsize ||
ntotal / xtotal / ytotal != ztotal ||
ntotal / xtotal / ztotal != ytotal ||
ntotal / ytotal / ztotal != xtotal ||
double_bytes / ntotal != sizeof (double) ||
size_bytes / ntotal != sizeof (size_t))
return ENOMEM;
/* Allocate the dynamic memory needed. */
ref->mem[0] = malloc(double_bytes);
ref->mem[1] = malloc(double_bytes);
ref->mem[2] = malloc(size_bytes);
if (!ref->mem[0] || !ref->mem[1] || !ref->mem[2]) {
free(ref->mem[2]);
ref->mem[2] = NULL;
free(ref->mem[1]);
ref->mem[1] = NULL;
free(ref->mem[0]);
ref->mem[0] = NULL;
return ENOMEM;
}
ref->xsize = xsize;
ref->ysize = ysize;
ref->zsize = zsize;
ref->ystride = xtotal;
ref->zstride = xtotal * ytotal;
ref->current = (double *)ref->mem[0] + 1 + xtotal;
ref->previous = (double *)ref->mem[1] + 1 + xtotal;
ref->fixup_cells = 0;
ref->fixup_index = (size_t *)ref->mem[2];
return 0;
}
обратите внимание, что я предпочитаю x + ystride * y + zstride * z
форма расчета индекса над x + xtotal * (y + ytotal * z)
, потому что два умножения в первом могут выполняться параллельно (в суперскалярном конвейере, на архитектурах, которые могут делать два несвязанные целочисленные умножения одновременно на одном ядре процессора), тогда как в последнем умножения должны быть последовательными.
отметим, что ref->current[-1 - ystride - zstride]
относится к текущему значению ячейки в ячейке (-1, -1, -1), т. е. диагонали ячейки границы от исходной ячейки (0, 0, 0). Другими словами, если у вас есть сотовый (x, y, z) по индексу i
, тогда
i-1
- это ячейка в (x-1, y, z)
i+1
- это ячейка в (x+1, y, z)
i-ystride
- это ячейка в (x, y-1, z)
i+ystride
- это ячейка в (x, y+1, z)
i-zstride
- это ячейка в (x, y, z-1)
i+zstride
- это ячейка в (x, y, z-1)
i-ystride
- это ячейка в (x, y-1, z)
i-1-ystride-zstride
- это ячейка в (x-1, y-1, z-1)
i+1+ystride+zstride
- это ячейка в (x+1, y+1, z+1)
и так далее.
на ref->fixup_index
массив достаточно большой, чтобы перечислить все клетки, за исключением пограничные ячейки. Это хорошая идея, чтобы сохранить его отсортированным (или сортировать его после его создания), потому что это помогает с локальностью кэша.
если ваша решетка имеет периодические граничные условия, вы можете использовать шесть 2D-петель, двенадцать 1D-петель и восемь копий для копирования первой и последней допустимых ячеек на границу перед началом нового обновления.
поэтому ваш цикл обновления по существу:
вычислить или заполнить границы в
->current
.обмен
->current
и->previous
.вычислить все ячейки для
->current
использование данных из->previous
.петли над
->fixup_cells
индексы в->fixup_index
, и пересчитать соответствующие->current
клетки.
обратите внимание, что на Шаге 3, Вы можете сделать это линейно для всех индексов между 0
и xsize-1 + (ysize-1)*ystride + (zsize-1)*zstride
, включительно; то есть, включая около 67% границы ячейки. Их относительно мало по сравнению со всем объемом, и наличие одного линейного цикла, вероятно, быстрее, чем пропуск через пограничные ячейки, особенно если вы можете векторизовать вычисления. (Что в данном случае нетривиально.)
вы даже можете разделить работу для нескольких потоков, предоставляя каждому потоку непрерывный набор индексов для работы. Потому что Вы читаете из ->previous
и писать ->current
потоки не затоптали друг друга, хотя могут быть некоторые если поток достигает конца своей области, в то время как другой находится в начале своей области; из-за того, как данные ориентированы (а строк кэша всего несколько-обычно 2, 4 или 8-ячеек), этот пинг-понг не должен быть проблемой на практике. (Очевидно, никакие замки не нужны.)
эта проблема является не Новой в любом случае. Моделирование Игра жизни Конвея или квадратная или кубическая решетчатая модель Изинга, а также реализация многих других решеточных моделей связана с той же проблемой (но часто с булевыми данными, а не двойниками, и без "плохих ячеек").
Я думаю, вы можете вложить пару подобных наборов петель. Что-то вроде этого:--2-->
for(int z = 1; z < numZ+1; z+=4) {
for(int y = 1; y < numY+1; y+=4) {
for(int x = 1; x < numX+1; x+=4) {
if(!isBadBlock[ getBlockIndex(x>>2,y>>2,z>>2) ]) {
for(int zz = z; zz < z + 4 && zz < numZ+1; zz++) {
for(int yy = y; yy < y + 4 && yy < numY+1; yy++) {
for(int xx = z; xx < x + 4 && xx < numX+1; xx++) {
if(!isBadCell[ getCellIndex(xx,yy,zz) ]) {
// Do stencil Computation
}
}
}
}
}
}
}
}
способ, которым вы в настоящее время его настроили, вы можете просто получить индекс, используя 3D-массив следующим образом:
#include <sys/types.h>
#define numX 256
#define numY 128
#define numZ 64
//Note the use of powers of 2 - it will simplify things a lot
int cells[numX][numY][numZ];
size_t getindex(size_t x, size_t y,size_t z){
return (int*)&cells[x][y][z]-(int*)&cells[0][0][0];
}
Это выложит клетки, как:
[0,0,0][0,0,1][0,0,2]...[0,0,numZ-1]
[0,1,0][0,1,1][0,1,2]...[0,1,numZ-1]
...
[0,numY-1,0][0,numY-1,1]...[0,1,numZ-1]
...
[1,0,0][1,0,1][0,0,2]...[1,0,numZ-1]
[1,1,0][1,1,1][1,1,2]...[1,1,numZ-1]
...
[numX-1,numY-1,0][numX-1,numY-1,1]...[numX-1,numY-1,numZ-1]
So efficient loops would look like:
for(size_t x=0;x<numX;x++)
for(size_t y=0;y<numY;y++)
for(size_t z=0;z<numZ;z++)
//vector operations on z values
но, если вы хотите разделить его на блоки 4x4x4, вы можете просто использовать 3D-массив из блоков 4x4x4 что-то вроде:
#include <sys/types.h>
#define numX 256
#define numY 128
#define numZ 64
typedef int block[4][4][4];
block blocks[numX][numY][numZ];
//add a compiler specific 64 byte alignment to help with cache misses?
size_t getblockindex(size_t x, size_t y,size_t z){
return (block *)&blocks[x][y][z]-(block *)&blocks[0][0][0];
}
Я переупорядочил индексы в x, y, z, чтобы я мог держать их прямо в голове, но убедитесь, что вы заказываете их так, чтобы последний из них был тем, который вы оперируете на серия в ваших сокровенных петлях.