Это хороший вопрос. Существует много причин, по которым вы хотели бы фактически перенести матрицу в память, а не просто менять координаты, например. в матричном умножении и гауссовском размытии.
Прежде всего позвольте мне перечислить одну из функций, которые я использую для транспонирования (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;
}
}
Этот способ, однако, будет иметь много недостатков в кэше. Гораздо быстрее, решение взять транспонирование В первой
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);
умножения матриц О (п^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];
}
}
EDIT:
Я попробовал несколько функций, чтобы найти самый быстрый транспонировать для больших матриц.В итоге самым быстрым результатом является использование блокировки цикла с помощью 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
Edit:
я нашел еще более быстрое решение с использованием 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);
}
}
}
}
}
Это называется «транспонирование». Вращение на 90 градусов - совершенно другое понятие. –
Кроме того, это не 90 градусов? Если бы это были первые две строки, это были бы 'm g a' и' n h b'. –
И самый быстрый способ - не вращать его, а просто менять индексный порядок при доступе к массиву. –