Это хороший вопрос. Есть много причин, по которым вы захотите фактически транспонировать матрицу в памяти, а не просто поменять местами координаты, например в матричном умножении и размытии по Гауссу.
Сначала позвольте мне перечислить одну из функций, которые я использую для транспонирования (РЕДАКТИРОВАТЬ: пожалуйста, посмотрите конец моего ответа, где я нашел гораздо более быстрое решение)
void transpose(float *src, float *dst, const int N, const int M) {
#pragma omp parallel for
for(int n = 0; n<N*M; n++) {
int i = n/N;
int j = n%N;
dst[n] = src[M*j + i];
}
}
Теперь посмотрим, почему транспонирование полезно. Рассмотрим умножение матриц C = A * B. Мы могли бы сделать это так.
for(int i=0; i<N; i++) {
for(int j=0; j<K; j++) {
float tmp = 0;
for(int l=0; l<M; l++) {
tmp += A[M*i+l]*B[K*l+j];
}
C[K*i + j] = tmp;
}
}
Таким образом, однако, будет много промахов в кеше. Гораздо более быстрое решение - сначала перенести букву B.
transpose(B);
for(int i=0; i<N; i++) {
for(int j=0; j<K; j++) {
float tmp = 0;
for(int l=0; l<M; l++) {
tmp += A[M*i+l]*B[K*j+l];
}
C[K*i + j] = tmp;
}
}
transpose(B);
Умножение матрицы - O (n ^ 3), а транспонирование - O (n ^ 2), поэтому транспонирование должно иметь незначительное влияние на время вычислений (для больших n
). В матричном умножении замощение петли даже более эффективно, чем транспонирование, но это намного сложнее.
Хотел бы я знать более быстрый способ выполнить транспонирование (Изменить: я нашел более быстрое решение, см. Конец моего ответа). Когда через несколько недель выйдет Haswell / AVX2, у него будет функция сбора данных. Не знаю, поможет ли это в данном случае, но я мог бы представить, как собираю столбец и записываю строку. Возможно, это сделает транспонирование ненужным.
Для смазывания по Гауссу вы делаете мазки по горизонтали, а затем по вертикали. Но размазывание по вертикали вызывает проблемы с кешем, поэтому вам нужно
Smear image horizontally
transpose output
Smear output horizontally
transpose output
Вот документ Intel, в котором объясняется, что http://software.intel.com/en-us/articles/iir-gaussian-blur-filter-implementation-using-intel-advanced-vector-extensions
Наконец, то, что я на самом деле делаю при умножении матриц (и при размытии по Гауссу), - это не точное транспонирование, а транспонирование по ширине определенного размера вектора (например, 4 или 8 для SSE / AVX). Вот функция, которую я использую
void reorder_matrix(const float* A, float* B, const int N, const int M, const int vec_size) {
#pragma omp parallel for
for(int n=0; n<M*N; n++) {
int k = vec_size*(n/N/vec_size);
int i = (n/vec_size)%N;
int j = n%vec_size;
B[n] = A[M*i + k + j];
}
}
РЕДАКТИРОВАТЬ:
Я попробовал несколько функций, чтобы найти наиболее быстрое транспонирование для больших матриц. В конце концов, самым быстрым результатом является использование блокировки цикла с block_size=16
(Изменить: я нашел более быстрое решение с использованием SSE и блокировки цикла - см. Ниже). Этот код работает для любой матрицы NxM (т.е. матрица не обязательно должна быть квадратной).
inline void transpose_scalar_block(float *A, float *B, const int lda, const int ldb, const int block_size) {
#pragma omp parallel for
for(int i=0; i<block_size; i++) {
for(int j=0; j<block_size; j++) {
B[j*ldb + i] = A[i*lda +j];
}
}
}
inline void transpose_block(float *A, float *B, const int n, const int m, const int lda, const int ldb, const int block_size) {
#pragma omp parallel for
for(int i=0; i<n; i+=block_size) {
for(int j=0; j<m; j+=block_size) {
transpose_scalar_block(&A[i*lda +j], &B[j*ldb + i], lda, ldb, block_size);
}
}
}
Значения lda
и ldb
- это ширина матрицы. Они должны быть кратны размеру блока. Чтобы найти значения и выделить память, например, матрица 3000х1001 делаю примерно так
#define ROUND_UP(x, s) (((x)+((s)-1)) & -(s))
const int n = 3000;
const int m = 1001;
int lda = ROUND_UP(m, 16);
int ldb = ROUND_UP(n, 16);
float *A = (float*)_mm_malloc(sizeof(float)*lda*ldb, 64);
float *B = (float*)_mm_malloc(sizeof(float)*lda*ldb, 64);
Для 3000x1001 это возвращает ldb = 3008
и lda = 1008
Изменить:
Я нашел еще более быстрое решение с использованием встроенных функций SSE:
inline void transpose4x4_SSE(float *A, float *B, const int lda, const int ldb) {
__m128 row1 = _mm_load_ps(&A[0*lda]);
__m128 row2 = _mm_load_ps(&A[1*lda]);
__m128 row3 = _mm_load_ps(&A[2*lda]);
__m128 row4 = _mm_load_ps(&A[3*lda]);
_MM_TRANSPOSE4_PS(row1, row2, row3, row4);
_mm_store_ps(&B[0*ldb], row1);
_mm_store_ps(&B[1*ldb], row2);
_mm_store_ps(&B[2*ldb], row3);
_mm_store_ps(&B[3*ldb], row4);
}
inline void transpose_block_SSE4x4(float *A, float *B, const int n, const int m, const int lda, const int ldb ,const int block_size) {
#pragma omp parallel for
for(int i=0; i<n; i+=block_size) {
for(int j=0; j<m; j+=block_size) {
int max_i2 = i+block_size < n ? i + block_size : n;
int max_j2 = j+block_size < m ? j + block_size : m;
for(int i2=i; i2<max_i2; i2+=4) {
for(int j2=j; j2<max_j2; j2+=4) {
transpose4x4_SSE(&A[i2*lda +j2], &B[j2*ldb + i2], lda, ldb);
}
}
}
}
}
person
Community
schedule
24.05.2013
m g a
иn h b
. - person Some programmer dude   schedule 24.05.2013_MM_TRANSPOSE()
. :-) - person Damon   schedule 24.05.2013