Эффективное преобразование прямоугольной матрицы произвольного размера во время выполнения
Мне не хватает времени, чтобы оптимизировать большой кусок кода на C для скорости, и я ищу алгоритм - в лучшем случае фрагмент "C " - который транспонирует прямоугольную исходную матрицу u[r][c]
произвольного размера (r
количество рядов, c
количество столбцов) в целевой матрице v[s][d]
(s = c
количество рядов, d = r
количество столбцов) в "дружественном к кэшу", т.е. в отношении данных. Типичный размер u
составляет около 5000 ... 15000 строк на 50–500 столбцов, и ясно, что построчный доступ к элементам очень неэффективен для кэширования.
Есть много дискуссий на эту тему в Интернете (рядом с этой веткой), но, насколько я вижу, все они обсуждают пространственные случаи, такие как квадратные матрицы, u[r][r]
или определение одномерного массива, например u[r * c]
, а не вышеупомянутый "массив массивов" (равной длины), используемый в моем контексте " Числовые рецепты" (фон см. здесь).
Я был бы очень благодарен за любой намек, который поможет мне избавиться от "переосмысления колеса".
Мартин
2 ответа
Я не думаю, что массив массивов гораздо сложнее транспонировать, чем линейный массив в целом. Но если у вас будет 50 столбцов в каждом массиве, это звучит плохо: этого может быть недостаточно, чтобы скрыть издержки разыменования указателя.
Я думаю, что общая стратегия реализации, дружественной к кешу, одна и та же: обработайте свою матрицу в тайлах, выберите размер тайлов, который лучше всего подходит для экспериментов.
template<int BLOCK>
void TransposeBlocked(Matrix &dst, const Matrix &src) {
int r = dst.r, c = dst.c;
assert(r == src.c && c == src.r);
for (int i = 0; i < r; i += BLOCK)
for (int j = 0; j < c; j += BLOCK) {
if (i + BLOCK <= r && j + BLOCK <= c)
ProcessFullBlock<BLOCK>(dst.data, src.data, i, j);
else
ProcessPartialBlock(dst.data, src.data, r, c, i, j, BLOCK);
}
}
Я попытался оптимизировать лучший случай, когда г = 10000, с = 500 (с float
тип). На моей локальной машине 128 х 128 плиток дают ускорение в 2,5 раза. Кроме того, я попытался использовать SSE для ускорения транспонирования, но это существенно не меняет время. Я думаю, что это потому, что проблема связана с памятью.
Вот полный набор времени (по 100 запусков в каждой) различных реализаций на Core2 E4700 2.6 ГГц:
Trivial: 6.111 sec
Blocked(4): 8.370 sec
Blocked(16): 3.934 sec
Blocked(64): 2.604 sec
Blocked(128): 2.441 sec
Blocked(256): 2.266 sec
BlockedSSE(16): 4.158 sec
BlockedSSE(64): 2.604 sec
BlockedSSE(128): 2.245 sec
BlockedSSE(256): 2.036 sec
Итак, я думаю, у вас есть массив массивов с плавающей запятой / двойников. Эта настройка уже очень плоха для производительности кеша. Причина в том, что с одномерным массивом компилятор может выводить код, который приводит к операции предварительной выборки и (в случае очень нового компилятора) генерировать SIMD/ векторизованный код. С массивом указателей на каждом шаге выполняется операция отсрочки, которая усложняет предварительную выборку. Не говоря уже о том, что нет никаких гарантий по выравниванию памяти.
Если это для назначения, и у вас нет другого выбора, кроме как написать код с нуля, я бы порекомендовал посмотреть, как это делает CBLAS (обратите внимание, что ваш массив все еще должен быть "сплющенным"). В противном случае вам будет гораздо лучше использовать высокооптимизированную реализацию BLAS, такую как OpenBLAS. Он был оптимизирован в течение почти десяти лет и будет производить самый быстрый код для вашего целевого процессора (настройка на такие вещи, как размеры кэша и набор векторных инструкций).
Tl;dr в том, что использование массива массивов приведет к ужасной производительности, несмотря ни на что. Выровняйте массивы и сделайте ваш код удобным для чтения, используя #define для доступа к элементам массива.