転置する必要のある行列(比較的大きい)があります。たとえば、私の行列が
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
これを行うための最速の方法は何ですか?
回答:
これは良い質問です。行列の乗算やガウススミアリングなど、座標を交換するだけでなく、実際に行列をメモリ内で転置したい理由はたくさんあります。
まず、転置に使用する関数の1つをリストします(編集:はるかに高速な解決策を見つけた私の回答の最後を参照してください)
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
これは、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
ます。このように、実際にはこれら2つのアプローチを組み合わせることができます。
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);
興味深い観察の1つは、このように2つのシャッフルを1つのシャッフルと2つのブレンド(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つ多くの命令を使用します。利点は、ポートの圧力を下げることです。これは、状況によってはメリットがある場合があります。現在、すべてのシャッフルとアンパックは1つの特定のポートにのみ移動できますが、ブレンドは2つの異なるポートのいずれかに移動できます。
MSVCのような8つのシャッフルを使用して、それを4つのシャッフル+ 8つのブレンドに変換しようとしましたが、機能しませんでした。私はまだ4つの開梱を使わなければなりませんでした。
私はこれと同じテクニックを8x8フロートトランスポーズに使用しました(その回答の終わり近くを参照)。 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が利用できない場合は、すべてをFPドメインでshufps
+blendps
で実行する価値があるかもしれませvpblendd
ん。また、Intel SnBファミリのハードウェアでは、のshufps
ような整数命令間で使用するための追加のバイパス遅延はありませんpaddd
。(ただし、Agner FogのSnBテストによるblendps
とpaddd
、との混合にはバイパス遅延があります。)
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ディスパッチが含まれています。これは、プログラムの実行時にハードウェアに最適な実装を選択します(移植性を損なうことなく)。
これは通常、ベクトル拡張組み込み関数を介して関数の手動最適化を実行するよりも優れた代替手段です。後者は、実装を特定のハードウェアベンダーとモデルに結び付けます。別のベンダー(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。これは1回のパスで実行できるため、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;
}