Це гарне запитання. Є багато причин, чому ви хотіли б фактично транспонувати матрицю в пам’ять, а не просто обмінюватись координатами, наприклад, при множенні матриці та розмиванні Гауса.
Спочатку дозвольте мені перерахувати одну з функцій, яку я використовую для транспонування ( EDIT: будь ласка, див. Кінець моєї відповіді, де я знайшов набагато швидше рішення )
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
- це ширина матриці. Вони повинні бути кратними розміру блоку. Щоб знайти значення та виділити пам'ять, наприклад, для матриці 3000x1001, я роблю щось подібне
#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);
}
}
}
}
}