2

Fortran で、多数の行列を異なる重みで乗算し、それらを合計して単一の行列を形成する関数を作成しようとしています。このプロセスが私のプログラムのボトルネックであることを確認しました (この重み付けは、プログラムの 1 回の実行に対して、さまざまな重み付けで度も行われます)。現在、Matlab から Fortran に切り替えて、より高速に実行しようとしています。私は Fortran の初心者なので、すべての助けに感謝します。

Matlab では、このような計算を行うために私が見つけた最速の方法は次のようになります。

function B = weight_matrices()
n = 46;
m = 1800;
A = rand(n,m,m);
w = rand(n,1);
tic;
B = squeeze(sum(bsxfun(@times,w,A),1));
toc;

が割り当てられている行Bは、私のマシン (Matlab R2012b、MacBook Pro 13" retina、2.5 GHz Intel Core i5、8 GB 1600 MHz DDR3) で約 0.9 秒で実行されます。私の問題では、テンソルAは(初期化後) プログラムの実行全体で同じ (定数) ですが, w は任意の値を取ることができます. また,ここではnとの典型的な値mが使用されています. つまり, テンソルAはメモリ内で約 1 GB のサイズを持つことになります.

これを Fortran で書く最も明確な方法は、次のようなものです。

pure function weight_matrices(w,A) result(B)
    implicit none
    integer, parameter :: n = 46
    integer, parameter :: m = 1800
    double precision, dimension(num_sizes), intent(in) :: w
    double precision, dimension(num_sizes,msize,msize), intent(in) :: A
    double precision, dimension(msize,msize) :: B
    integer :: i
    B = 0
    do i = 1,n
        B = B + w(i)*A(i,:,:)
    end do
end function weight_matrices

この関数は、-O3 を使用して gfortran 4.7.2 でコンパイルすると、約 1.4 秒で実行されます (「call cpu_time(t)」で時間指定された関数呼び出し)。ループを手動でアンラップすると

B = w(1)*A(1,:,:)+w(2)*A(2,:,:)+ ... + w(46)*A(46,:,:)

代わりに、関数の実行に約 0.11 秒かかります。これは素晴らしいことで、Matlab バージョンと比較して約 8 倍のスピードアップが得られることを意味します。ただし、読みやすさとパフォーマンスについてはまだ疑問があります。

まず、この重み付けと行列の合計を実行するさらに高速な方法があるかどうか疑問に思います。BLAS と LAPACK を調べましたが、適合すると思われる関数が見つかりません。また、行列を列挙する次元を最後の次元として配置しようとしましたA(つまり、要素に対して から(i,j,k)に切り替え(k,i,j)ました) が、これによりコードが遅くなりました。

第 2 に、この高速バージョンはあまり柔軟ではなく、実際には非常に見栄えが悪くなります。これは、このような単純な計算に対してテキストが多すぎるためです。私が実行しているテストでは、さまざまな数の重みを使用して、w の長さが変化し、アルゴリズムの残りの部分にどのように影響するかを確認したいと思います。Bとはいえ、毎回の課題の書き直しって結構面倒なんですよね。パフォーマンスを同じ (またはそれ以上) に保ちながら、これをより柔軟にする方法はありますか?

第三に、テンソルAは、前述のように、プログラムの実行中は一定です。独自のモジュールで「パラメーター」属性を使用してプログラムに定数スカラー値を設定し、それらを必要とする関数/サブルーチンに「use」式でインポートしました。テンソルに対して同等のことを行う最良の方法は何Aですか? 対応する最適化を実行できるように、このテンソルは初期化後に定数になることをコンパイラに伝えたいと思います。A通常、サイズは約 1 GB であるため、ソース ファイルに直接入力するのは現実的ではないことに注意してください。

ご意見をお寄せいただきありがとうございます。:)

4

4 に答える 4

3

おそらく、次のようなものを試すことができます

    do k=1,m
       do j=1,m
          B(j,k)=sum( [ ( (w(i)*A(i,j,k)), i=1,n) ])
       enddo
    enddo

角括弧は (/ /) の新しい形式で、1 次元行列 (ベクトル) です。項sumは次元の行列であり、それらの要素すべて(n)sum合計します。これはまさに、ラップされていないコードが行うことです (そして、doループと完全に同じではありません)。

于 2013-04-20T21:05:35.793 に答える
1

これは通常遅いため、ループを隠しません。明示的に記述すると、内部ループ アクセスが最後のインデックスを超えて非効率になることがわかります。したがって、nA is を保存して、次元が最後のものであることを確認する必要がありA(m,m,n)ます。

B = 0
do i = 1,n
    w_tmp = w(i)
    do j = 1,m
        do k = 1,m
            B(k,j) = B(k,j) + w_tmp*A(k,j,i)
        end do
    end do
end do

内側のループでメモリ内の連続する要素にアクセスしているため、これははるかに効率的です。

もう 1 つの解決策は、レベル 1 BLAS サブルーチン _AXPY (y = a*x + y) を使用することです。

B = 0
do i = 1,n
    CALL DAXPY(m*m, w(i), A(1,1,i), 1, B(1,1), 1)
end do

インテル® MKL を使用すると、これはより効率的になりますが、最後のインデックスが外側のループ (この場合は作成中のループ) で変更されるものであることを確認する必要があります。この呼び出しに必要な引数は、MKLにあります。

編集:並列化を使用することもできますか? (Matlab がそれを利用しているかどうかはわかりません)

EDIT2: Kyle の回答では、内側のループは のさまざまな値を超えています。これは、( を使用して)キャッシュに保持できるため、リロードするwよりも効率的です。nBwA(n,m,m)

B = 0
do i = 1,m
    do j = 1,m
        B(j,i)=0.0d0
        do k = 1,n
            B(j,i) = B(j,i) + w(k)*A(k,j,i)
        end do
    end do
end do

この明示的なループは、配列全体の操作を使用する Kyle のコードよりも約 10% 優れたパフォーマンスを発揮します。の帯域幅ifort -O3 -xHostは ~6600 MB/s で、gfortran -O3それは ~6000 MB/s であり、いずれかのコンパイラを使用したアレイ全体のバージョンも約 6000 MB/s です。

于 2013-04-21T08:48:06.817 に答える