web-dev-qa-db-ja.com

arrayfunは、matlabの明示的なループよりもかなり遅くなる可能性があります。どうして?

arrayfunの次の簡単な速度テストを検討してください。

_T = 4000;
N = 500;
x = randn(T, N);
Func1 = @(a) (3*a^2 + 2*a - 1);

tic
Soln1 = ones(T, N);
for t = 1:T
    for n = 1:N
        Soln1(t, n) = Func1(x(t, n));
    end
end
toc

tic
Soln2 = arrayfun(Func1, x);
toc
_

私のマシン(Linux Mint 12上のMatlab 2011b)では、このテストの出力は次のとおりです。

_Elapsed time is 1.020689 seconds.
Elapsed time is 9.248388 seconds.
_

何だ!? arrayfunは、明らかにクリーンなソリューションですが、1桁遅くなります。ここで何が起こっていますか?

さらに、cellfunに対して同様のスタイルのテストを行ったところ、明示的なループよりも約3倍遅いことがわかりました。繰り返しますが、この結果は私が期待したものとは逆です。

私の質問は:なぜarrayfuncellfunがそんなに遅いのですか?そして、これを考えると、それらを使用する正当な理由はありますか(コードの見栄えを良くする以外に)?

注:ここでは、並列処理ツールボックスのGPUバージョンではなく、arrayfunの標準バージョンについて説明しています。

編集:明確にするために、上記の_Func1_はOliが指摘したようにベクトル化できることを認識しています。私が選んだのは、実際の質問のために簡単な速度テストが得られるからです。

編集: grungettaの提案に従って、_feature accel off_を使用してテストを再決定しました。結果は次のとおりです。

_Elapsed time is 28.183422 seconds.
Elapsed time is 23.525251 seconds.
_

言い換えれば、違いの大部分は、JITアクセラレータがforよりも明示的なarrayfunループを高速化するという点で優れているということです。 arrayfunが実際により多くの情報を提供するため、これは奇妙に思えます。つまり、その使用により、_Func1_の呼び出しの順序は重要ではないことがわかります。また、JITアクセラレータのオン/オフに関係なく、システムは1つのCPUのみを使用することに注意してください...

101
Colin T Bowers

他のバージョンのコードを実行して、アイデアを得ることができます。ループで関数を使用する代わりに、計算を明示的に書き出すことを検討してください

tic
Soln3 = ones(T, N);
for t = 1:T
    for n = 1:N
        Soln3(t, n) = 3*x(t, n)^2 + 2*x(t, n) - 1;
    end
end
toc

コンピューターで計算する時間:

Soln1  1.158446 seconds.
Soln2  10.392475 seconds.
Soln3  0.239023 seconds.
Oli    0.010672 seconds.

今、完全に「ベクトル化された」ソリューションは明らかに最速ですが、xエントリごとに呼び出される関数を定義することはhugeオーバーヘッドであることがわかります。計算を明示的に書き出すだけで、ファクター5の速度が向上しました。これは、MATLABのJITコンパイラ インライン関数をサポートしない を示していると思います。そこのgnoviceの答えによると、実際には匿名の関数よりも通常の関数を書く方が良いです。それを試してみてください。

次のステップ-内側のループを削除(ベクトル化)します:

tic
Soln4 = ones(T, N);
for t = 1:T
    Soln4(t, :) = 3*x(t, :).^2 + 2*x(t, :) - 1;
end
toc

Soln4  0.053926 seconds.

もう1つの要因5の高速化:これらのステートメントには、MATLABでのループを回避する必要があることを示すものがあります...または、本当にありますか?これを見てください

tic
Soln5 = ones(T, N);
for n = 1:N
    Soln5(:, n) = 3*x(:, n).^2 + 2*x(:, n) - 1;
end
toc

Soln5   0.013875 seconds.

「完全に」ベクトル化されたバージョンにはるかに近い。 Matlabは行列を列ごとに保存します。常に(可能であれば)計算を「列単位」でベクトル化されるように構成する必要があります。

Soln3に戻ることができます。ループの順序は「行ごと」です。それを変えましょう

tic
Soln6 = ones(T, N);
for n = 1:N
    for t = 1:T
        Soln6(t, n) = 3*x(t, n)^2 + 2*x(t, n) - 1;
    end
end
toc

Soln6  0.201661 seconds.

より良いが、それでも非常に悪い。シングルループ-良い。ダブルループ-悪い。 MATLABはループのパフォーマンスを改善するためにある程度の作業を行ったと思いますが、それでもループのオーバーヘッドは残っています。内部でより重い作業を行う場合、気付かないでしょう。ただし、この計算はメモリ帯域幅に制限されているため、ループのオーバーヘッドが発生します。そして、あなたはそこにFunc1を呼び出すオーバーヘッドをさらに明確に見るでしょう。

では、arrayfunはどうなっていますか?そこには関数もありませんので、多くのオーバーヘッドがあります。しかし、なぜ二重入れ子ループよりもずっと悪いのでしょうか?実際、cellfun/arrayfunの使用に関するトピックは何度も広範囲に議論されてきました(例 hereherehere および here )。これらの関数は単純に遅いため、このようなきめ細かい計算には使用できません。これらを使用して、コードを簡潔にし、セルと配列の間の高度な変換を行うことができます。しかし、関数はあなたが書いたものより重い必要があります:

tic
Soln7 = arrayfun(@(a)(3*x(:,a).^2 + 2*x(:,a) - 1), 1:N, 'UniformOutput', false);
toc

Soln7  0.016786 seconds.

Soln7は現在セルであることに注意してください。時にはそれが役立つこともあります。コードのパフォーマンスは現在非常に良好であり、出力としてセルが必要な場合は、完全にベクトル化されたソリューションを使用した後にマトリックスを変換する必要はありません。

では、なぜarrayfunは単純なループ構造よりも遅いのでしょうか?残念ながら、利用可能なソースコードがないため、私たちが確実に言うことは不可能です。 arrayfunは、あらゆる種類の異なるデータ構造と引数を処理する汎用関数であるため、ループネストとして直接表現できる単純なケースでは必ずしも高速ではないと推測できます。オーバーヘッドはどこから来るのかわかりません。より良い実装によってオーバーヘッドを回避できますか?そうでないかもしれない。しかし、残念なことに、私たちにできることは、パフォーマンスを調べて、うまく機能するケースとそうでないケースを特定することだけです。

Updateこのテストの実行時間は短いため、信頼できる結果を得るために、テストの周りにループを追加しました。

for i=1:1000
   % compute
end

以下に示すいくつかの時間:

Soln5   8.192912 seconds.
Soln7  13.419675 seconds.
Oli     8.089113 seconds.

Arrayfunはまだ悪いですが、ベクトル化されたソリューションよりも少なくとも3桁悪くないことがわかります。一方、列ごとの計算を行う単一ループは、完全にベクトル化されたバージョンと同じくらい高速です...それはすべて単一のCPUで実行されました。 Soln5とSoln7の結果は、2つのコアに切り替えても変わりません-Soln5では、parforを使用して並列化する必要があります。スピードアップを忘れてください... arrayfunは並行して実行されないため、Soln7は並行して実行されません。一方、オリスはベクトル化されたバージョン:

Oli  5.508085 seconds.
101
angainor