C++: повышение производительности кэша в 3D массиве

Я не знаю, как оптимизировать производительность кэша на очень низком уровне, думая о размере строки кэша или ассоциативности. Это не то, чему можно научиться за одну ночь. Учитывая, что моя программа будет работать на многих разных системах и архитектурах, я не думаю, что это будет стоить того. Но все же, вероятно, есть некоторые шаги, которые я могу предпринять, чтобы уменьшить пропуски кэша в целом.

Вот описание моей проблемы:

у меня есть 3D-массив целых чисел, представляющих значения в точках пространства, например [x] [y] [z]. Каждое измерение имеет одинаковый размер, поэтому оно похоже на куб. Из этого мне нужно сделать еще один 3D-массив, где каждое значение в этом новом массиве является функцией 7 параметров: соответствующее значение в исходном 3d-массиве плюс 6 индексов, которые "касаются" его в пространстве. Я пока не беспокоюсь о краях и углах Куба.

вот что я имею в виду в C++ код:

void process3DArray (int input[LENGTH][LENGTH][LENGTH], 
                     int output[LENGTH][LENGTH][LENGTH])
{
    for(int i = 1; i < LENGTH-1; i++)
        for (int j = 1; j < LENGTH-1; j++)
            for (int k = 1; k < LENGTH-1; k++)
            //The for loops start at 1 and stop before LENGTH-1
            //or other-wise I'll get out-of-bounds errors
            //I'm not concerned with the edges and corners of the 
            //3d array "cube" at the moment.
            {
                int value = input[i][j][k];

                //I am expecting crazy cache misses here:
                int posX = input[i+1] [j]   [k];
                int negX = input[i-1] [j]   [k];
                int posY = input[i]   [j+1] [k];
                int negY = input[i]   [j-1] [k];
                int posZ = input[i]   [j]   [k+1];
                int negZ = input[i]   [j]   [k-1];

                output [i][j][k] = 
                    process(value, posX, negX, posY, negY, posZ, negZ);
            }
}

однако, кажется, что если длина достаточно велика, я получите тонны пропусков кэша, когда я получаю параметры для process. Есть ли более удобный для кэша способ сделать это или лучший способ представить мои данные, отличные от 3D-массива?

и если у вас есть время, чтобы ответить на эти дополнительные вопросы, я должен считать значение длина? Как будто это отличается, является ли длина 20 против 100 против 10000. Кроме того, должен ли я делать что-то еще, если я использую что-то другое, кроме целых чисел, например, 64-байтовую структуру?

@ ildjarn:

Извините, я не думал, что код, который генерирует массивы, я передаю в process3DArray имело значения. Но если это так, я хотел бы знать почему.

int main() {
    int data[LENGTH][LENGTH][LENGTH];
    for(int i = 0; i < LENGTH; i++)
        for (int j = 0; j < LENGTH; j++)
            for (int k = 0; k < LENGTH; k++)
                data[i][j][k] = rand() * (i + j + k);

    int result[LENGTH][LENGTH][LENGTH];
    process3DArray(data, result);
}

2 ответов


самое главное, что вы уже имеете право. Если бы вы использовали Fortran, вы бы делали это совершенно неправильно, но это другая история. То, что вы имеете право, - это то, что вы обрабатываете во внутреннем цикле вдоль направления, где адреса памяти ближе всего друг к другу. Одна выборка памяти (за пределами кэша) будет вытягивать несколько значений, соответствующих ряду соседних значений k. Внутри вашего цикла кэш будет содержать некоторое количество значений из i, j; аналогичное число из i+/-1, j и из i,j+ / -1. Таким образом, у вас в основном есть пять непересекающихся активных разделов памяти. Для небольших значений длины это будет только 1 или три раздела памяти. В природе того, как создаются кэши, вы можете иметь больше, чем это количество непересекающихся разделов памяти в активном наборе.

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

С вас интересует производительность, почти всегда лучше инициализировать пять указателей(вам нужен только один для value, posZ и negZ), а затем взять *(p++) внутри цикла.

input[i+1] [j]   [k];

просит компилятор генерировать 3 добавления и два умножения, если у вас нет очень хорошего оптимизатора. Если ваш компилятор особенно ленив в выделении регистра, вы также получаете четыре доступа к памяти; в противном случае один.

*inputIplusOneJK++ 

просит добавить и памяти ссылка.


здесь есть ответ на аналогичный вопрос:https://stackoverflow.com/a/7735362/6210 (мной!)

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

Так как вы делая что-то более сложное, чем простой обход элемента (посещение элемента плюс 6 соседей), вам нужно разбить свой обход так, чтобы вы не получали доступ к слишком многим строкам кэша сразу. Поскольку в кеше thrashing преобладает перемещение вдоль j и k, вам просто нужно изменить обход так, чтобы вы посещали блоки за раз, а не строки за раз.

например:

const int CACHE_LINE_STEP= 8;

void process3DArray (int input[LENGTH][LENGTH][LENGTH], 
                     int output[LENGTH][LENGTH][LENGTH])
{
    for(int i = 1; i < LENGTH-1; i++)
        for (int k_start = 1, k_next= CACHE_LINE_STEP; k_start < LENGTH-1; k_start= k_next; k_next+= CACHE_LINE_STEP)
        {
            int k_end= min(k_next, LENGTH - 1);

            for (int j = 1; j < LENGTH-1; j++)
                //The for loops start at 1 and stop before LENGTH-1
                //or other-wise I'll get out-of-bounds errors
                //I'm not concerned with the edges and corners of the 
                //3d array "cube" at the moment.
            {
                for (int k= k_start; k<k_end; ++k)
                {
                    int value = input[i][j][k];

                    //I am expecting crazy cache misses here:
                    int posX = input[i+1] [j]   [k];
                    int negX = input[i-1] [j]   [k];
                    int posY = input[i]   [j+1] [k];
                    int negY = input[i]   [j-1] [k];
                    int posZ = input[i]   [j]   [k+1];
                    int negZ = input[i]   [j]   [k-1];

                    output [i][j][k] = 
                        process(value, posX, negX, posY, negY, posZ, negZ);
                }
            }
        }
}

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