C ++에서 행렬을 전치하는 가장 빠른 방법은 무엇입니까? i j k l m n

전치해야 할 행렬 (상대적으로 큰)이 있습니다. 예를 들어 내 행렬이

a b c d e f
g h i j k l
m n o p q r

결과는 다음과 같습니다.

a g m
b h n
c I o
d j p
e k q
f l r

이를 수행하는 가장 빠른 방법은 무엇입니까?



답변

이것은 좋은 질문입니다. 행렬 곱셈 및 가우스 스미어 링과 같이 좌표를 바꾸는 것보다 실제로 행렬을 메모리에서 전치하려는 많은 이유가 있습니다.

먼저 전치에 사용하는 기능 중 하나를 나열하겠습니다 ( 편집 : 훨씬 빠른 솔루션을 찾은 내 대답의 끝 부분을 참조하십시오 )

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)이므로 전치를 수행하면 계산 시간에 무시할 수있는 영향을 미칩니다 (large n). 행렬 곱셈에서 루프 타일링은 전치하는 것보다 훨씬 효과적이지만 훨씬 더 복잡합니다.

조옮김을 수행하는 더 빠른 방법을 알고 싶습니다 ( 편집 : 더 빠른 솔루션을 찾았습니다 . 내 대답 끝 참조 ). Haswell / AVX2가 몇 주 안에 출시되면 수집 기능이 있습니다. 이 경우에 도움이 될지는 모르겠지만 열을 모으고 행을 쓰는 이미지를 만들 수 있습니다. 아마도 조옮김을 불필요하게 만들 것입니다.

가우시안 스미어 링의 경우 수평으로 스미어 한 다음 수직으로 스미어 링합니다. 하지만 수직으로 번지는 것은 캐시 문제가 있으므로

Smear image horizontally
transpose output
Smear output horizontally
transpose output

다음은 http://software.intel.com/en-us/articles/iir-gaussian-blur-filter-implementation-using-intel-advanced-vector-extensions를 설명하는 Intel의 문서입니다
.

마지막으로 매트릭스 곱셈 (및 가우스 스미어 링)에서 실제로 수행하는 작업은 정확히 전치하는 것이 아니라 특정 벡터 크기 (예 : SSE / AVX의 경우 4 또는 8)의 너비로 전치하는 것입니다. 내가 사용하는 기능은 다음과 같습니다.

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);
        }
    }
}

ldaldb는 행렬의 너비입니다. 블록 크기의 배수 여야합니다. 값을 찾고 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);

3000×1001의 경우 다음이 반환 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);
                }
            }
        }
    }
}

답변

이것은 응용 프로그램에 따라 다르지만 일반적으로 행렬을 전치하는 가장 빠른 방법은 조회 할 때 좌표를 반전하는 것입니다. 그러면 실제로 데이터를 이동할 필요가 없습니다.


답변

x86 하드웨어를 사용하여 4×4 제곱 부동 (나중에 32 비트 정수에 대해 설명 함) 행렬을 바꾸는 방법에 대한 세부 정보입니다. 8×8 또는 16×16과 같은 더 큰 정사각형 행렬을 전치하려면 여기에서 시작하는 것이 좋습니다.

_MM_TRANSPOSE4_PS(r0, r1, r2, r3)다른 컴파일러에 의해 다르게 구현됩니다. GCC 및 ICC (Clang을 확인하지 않았습니다)는 사용 unpcklps, unpckhps, unpcklpd, unpckhpd하지만 MSVC는 shufps. 우리는 실제로이 두 가지 접근법을 이렇게 결합 할 수 있습니다.

t0 = _mm_unpacklo_ps(r0, r1);
t1 = _mm_unpackhi_ps(r0, r1);
t2 = _mm_unpacklo_ps(r2, r3);
t3 = _mm_unpackhi_ps(r2, r3);

r0 = _mm_shuffle_ps(t0,t2, 0x44);
r1 = _mm_shuffle_ps(t0,t2, 0xEE);
r2 = _mm_shuffle_ps(t1,t3, 0x44);
r3 = _mm_shuffle_ps(t1,t3, 0xEE);

흥미로운 점은 두 개의 셔플이 이와 같이 하나의 셔플과 두 개의 블렌드 (SSE4.1)로 변환 될 수 있다는 것입니다.

t0 = _mm_unpacklo_ps(r0, r1);
t1 = _mm_unpackhi_ps(r0, r1);
t2 = _mm_unpacklo_ps(r2, r3);
t3 = _mm_unpackhi_ps(r2, r3);

v  = _mm_shuffle_ps(t0,t2, 0x4E);
r0 = _mm_blend_ps(t0,v, 0xC);
r1 = _mm_blend_ps(t2,v, 0x3);
v  = _mm_shuffle_ps(t1,t3, 0x4E);
r2 = _mm_blend_ps(t1,v, 0xC);
r3 = _mm_blend_ps(t3,v, 0x3);

이것은 4 개의 셔플을 2 개의 셔플과 4 개의 블렌드로 효과적으로 변환했습니다. 이것은 GCC, ICC 및 MSVC의 구현보다 2 개의 명령어를 더 사용합니다. 장점은 일부 상황에서 이점을 가질 수있는 포트 압력을 감소 시킨다는 것입니다. 현재 모든 셔플 및 압축 해제는 하나의 특정 포트로만 이동할 수 있지만 블렌드는 두 개의 다른 포트 중 하나로 이동할 수 있습니다.

MSVC와 같은 8 개의 셔플을 사용하여 4 개의 셔플 + 8 개의 블렌드로 변환하려고 시도했지만 작동하지 않았습니다. 나는 여전히 4 번의 포장을 풀어야했다.

나는이 동일한 기술을 8×8 float 전치에 사용했습니다 (해답의 끝 부분 참조).
https://stackoverflow.com/a/25627536/2542702 . 그 대답에서 나는 여전히 8 개의 압축 풀기를 사용해야했지만 8 개의 셔플을 4 개의 셔플과 8 개의 블렌드로 변환하도록 관리했습니다.

32 비트 정수의 경우 shufps(AVX512를 사용한 128 비트 셔플을 제외하고) 같은 것이 없으므로 블렌드로 변환 할 수 없다고 생각하는 언팩으로 만 구현할 수 있습니다 (효율적으로). AVX512 vshufi32x4shufps32 비트 부동 소수점 대신 4 개의 정수로 구성된 128 비트 레인을 제외하고 는 효과적으로 작동 하므로이 동일한 기술이 경우에 따라 가능할 수 있습니다 vshufi32x4. Knights Landing의 경우 셔플은 혼합보다 4 배 더 느립니다 (처리량).


답변

각 행을 열로, 각 열을 행으로 간주합니다. .. i, j 대신 j, i 사용

데모 : http://ideone.com/lvsxKZ

#include <iostream> 
using namespace std;

int main ()
{
    char A [3][3] =
    {
        { 'a', 'b', 'c' },
        { 'd', 'e', 'f' },
        { 'g', 'h', 'i' }
    };

    cout << "A = " << endl << endl;

    // print matrix A
    for (int i=0; i<3; i++)
    {
        for (int j=0; j<3; j++) cout << A[i][j];
        cout << endl;
    }

    cout << endl << "A transpose = " << endl << endl;

    // print A transpose
    for (int i=0; i<3; i++)
    {
        for (int j=0; j<3; j++) cout << A[j][i];
        cout << endl;
    }

    return 0;
}

답변

오버 헤드없이 전치 (클래스가 완료되지 않음) :

class Matrix{
   double *data; //suppose this will point to data
   double _get1(int i, int j){return data[i*M+j];} //used to access normally
   double _get2(int i, int j){return data[j*N+i];} //used when transposed

   public:
   int M, N; //dimensions
   double (*get_p)(int, int); //functor to access elements  
   Matrix(int _M,int _N):M(_M), N(_N){
     //allocate data
     get_p=&Matrix::_get1; // initialised with normal access 
     }

   double get(int i, int j){
     //there should be a way to directly use get_p to call. but i think even this
     //doesnt incur overhead because it is inline and the compiler should be intelligent
     //enough to remove the extra call
     return (this->*get_p)(i,j);
    }
   void transpose(){ //twice transpose gives the original
     if(get_p==&Matrix::get1) get_p=&Matrix::_get2;
     else get_p==&Matrix::_get1;
     swap(M,N);
     }
}

다음과 같이 사용할 수 있습니다.

Matrix M(100,200);
double x=M.get(17,45);
M.transpose();
x=M.get(17,45); // = original M(45,17)

물론 중요하지만 다른 주제 인 메모리 관리에 대해서는 신경 쓰지 않았습니다.


답변

배열의 크기를 미리 알고있는 경우 통합을 사용하여 도움을받을 수 있습니다. 이렇게-

#include <bits/stdc++.h>
using namespace std;

union ua{
    int arr[2][3];
    int brr[3][2];
};

int main() {
    union ua uav;
    int karr[2][3] = {{1,2,3},{4,5,6}};
    memcpy(uav.arr,karr,sizeof(karr));
    for (int i=0;i<3;i++)
    {
        for (int j=0;j<2;j++)
            cout<<uav.brr[i][j]<<" ";
        cout<<'\n';
    }

    return 0;
}

답변

template <class T>
void transpose( const std::vector< std::vector<T> > & a,
std::vector< std::vector<T> > & b,
int width, int height)
{
    for (int i = 0; i < width; i++)
    {
        for (int j = 0; j < height; j++)
        {
            b[j][i] = a[i][j];
        }
    }
}