전치해야 할 행렬 (상대적으로 큰)이 있습니다. 예를 들어 내 행렬이
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
이를 수행하는 가장 빠른 방법은 무엇입니까?
전치해야 할 행렬 (상대적으로 큰)이 있습니다. 예를 들어 내 행렬이
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);
}
}
}
값 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);
}
}
}
}
}
이것은 응용 프로그램에 따라 다르지만 일반적으로 행렬을 전치하는 가장 빠른 방법은 조회 할 때 좌표를 반전하는 것입니다. 그러면 실제로 데이터를 이동할 필요가 없습니다.
(i,j)
매핑 될 것입니다(j,i)
x86 하드웨어를 사용하여 4x4 제곱 부동 (나중에 32 비트 정수에 대해 설명 함) 행렬을 바꾸는 방법에 대한 세부 정보입니다. 8x8 또는 16x16과 같은 더 큰 정사각형 행렬을 전치하려면 여기에서 시작하는 것이 좋습니다.
_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 번의 포장을 풀어야했다.
나는이 동일한 기술을 8x8 float 전치에 사용했습니다 (해답의 끝 부분 참조). https://stackoverflow.com/a/25627536/2542702 . 그 대답에서 나는 여전히 8 개의 압축 풀기를 사용해야했지만 8 개의 셔플을 4 개의 셔플과 8 개의 블렌드로 변환하도록 관리했습니다.
32 비트 정수의 경우 shufps
(AVX512를 사용한 128 비트 셔플을 제외하고) 같은 것이 없으므로 블렌드로 변환 할 수 없다고 생각하는 언팩으로 만 구현할 수 있습니다 (효율적으로). AVX512 vshufi32x4
는 shufps
32 비트 부동 소수점 대신 4 개의 정수로 구성된 128 비트 레인을 제외하고 는 효과적으로 작동 하므로이 동일한 기술이 경우에 따라 가능할 수 있습니다 vshufi32x4
. Knights Landing의 경우 셔플은 혼합보다 4 배 더 느립니다 (처리량).
shufps
정수 데이터에 사용할 수 있습니다 . 셔플 링을 많이하는 경우, 특히 똑같이 효율적인 AVX2를 사용할 수 없는 경우 shufps
+ 에 대해 FP 도메인에서 모든 작업을 수행하는 것이 blendps
좋습니다 vpblendd
. 또한 Intel SnB 제품군 하드웨어에서는 .NET shufps
과 같은 정수 명령어간에 사용하기위한 추가 바이 패스 지연이 없습니다 paddd
. (혼합 바이 패스 지연이 blendps
와 함께 paddd
하지만, Agner 안개의 SNB 테스트에 따르면.)
vinsertf64x4
아니라 16x16 전치 대답에서 계속 언급했는지 알 수 있습니다 vinserti64x4
. 행렬을 읽고 쓰는 경우 전치가 데이터를 이동하기 때문에 부동 소수점 도메인 또는 정수 도메인을 사용하는지 여부는 확실히 중요하지 않습니다.
각 행을 열로, 각 열을 행으로 간주합니다. .. i, j 대신 j, i 사용
#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];
}
}
}
최신 선형 대수 라이브러리에는 가장 일반적인 작업의 최적화 된 버전이 포함되어 있습니다. 이들 중 다수는 동적 CPU 디스패치를 포함하여 프로그램 실행시 하드웨어에 대한 최상의 구현을 선택합니다 (이동성 저하없이).
이것은 일반적으로 벡터 확장 내장 함수를 통해 functinos의 수동 최적화를 수행하는 것보다 나은 대안입니다. 후자는 구현을 특정 하드웨어 공급 업체 및 모델에 연결합니다. 다른 공급 업체 (예 : Power, ARM) 또는 새로운 벡터 확장 (예 : AVX512)으로 교체하기로 결정한 경우이를 다시 구현해야합니다. 최대한 활용하십시오.
예를 들어 MKL 전치에는 BLAS 확장 기능이 포함됩니다 imatcopy
. OpenBLAS와 같은 다른 구현에서도 찾을 수 있습니다.
#include <mkl.h>
void transpose( float* a, int n, int m ) {
const char row_major = 'R';
const char transpose = 'T';
const float alpha = 1.0f;
mkl_simatcopy (row_major, transpose, n, m, alpha, a, n, n);
}
C ++ 프로젝트의 경우 Armadillo C ++를 사용할 수 있습니다.
#include <armadillo>
void transpose( arma::mat &matrix ) {
arma::inplace_trans(matrix);
}
나는 가장 빠른 방법이 O (n ^ 2)보다 높게 취해서는 안된다고 생각합니다. 이런 식으로도 O (1) 공간 만 사용할 수 있습니다.
그렇게하는 방법은 쌍으로 교환하는 것입니다. 왜냐하면 행렬을 전치 할 때 M [i] [j] = M [j] [i]이므로 M [i] [j]를 temp에 저장 한 다음 M [i] [j] = M [j] [i], 마지막 단계 : M [j] [i] = temp. 이것은 한 번의 패스로 할 수 있으므로 O (n ^ 2)
내 대답은 3x3 행렬로 바뀝니다.
#include<iostream.h>
#include<math.h>
main()
{
int a[3][3];
int b[3];
cout<<"You must give us an array 3x3 and then we will give you Transposed it "<<endl;
for(int i=0;i<3;i++)
{
for(int j=0;j<3;j++)
{
cout<<"Enter a["<<i<<"]["<<j<<"]: ";
cin>>a[i][j];
}
}
cout<<"Matrix you entered is :"<<endl;
for (int e = 0 ; e < 3 ; e++ )
{
for ( int f = 0 ; f < 3 ; f++ )
cout << a[e][f] << "\t";
cout << endl;
}
cout<<"\nTransposed of matrix you entered is :"<<endl;
for (int c = 0 ; c < 3 ; c++ )
{
for ( int d = 0 ; d < 3 ; d++ )
cout << a[d][c] << "\t";
cout << endl;
}
return 0;
}