1. ホーム
  2. arrays

[解決済み] arrayfunはmatlabの明示的なループよりかなり遅くなることがあります. なぜでしょうか?

2022-10-29 14:57:25

疑問点

次のような簡単なスピードテストについて考えてみましょう。 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 は、確かに見た目はすっきりしていますが、速度は一桁遅くなります。どうなっているのでしょうか?

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

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

注意してください。 私が言っているのは、標準的なバージョンの arrayfun ここでは、並列処理ツールボックスのGPUバージョンではなく、標準バージョンについて話しています。

EDITです。 念のため、私が認識しているのは Func1 はOliさんが指摘されたようにベクトル化することができます。私は、それが実際の質問の目的のための単純な速度テストをもたらすので、それを選んだだけです。

EDITです。 grungettaさんの提案に従って、私はテストを再実行しました。 feature accel off . 結果は以下の通りです。

Elapsed time is 28.183422 seconds.
Elapsed time is 23.525251 seconds.

言い換えれば、JITアクセラレータが、明示的な for ループを高速化することが arrayfun . これは私には奇妙に思えます。 arrayfun への呼び出しの順番を明らかにするものだからです。 Func1 の呼び出しの順番は重要ではないことがわかります。また、JIT アクセラレーターがオンであろうとオフであろうと、私のシステムは 1 つの CPU しか使用しないことに注目しました。

どのように解決するのですか?

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

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 エントリに対して呼び出される関数を定義することは 巨大な オーバーヘッドです。計算を明示的に書き出すだけで、ファクター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.

もうひとつの高速化要因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に戻ることができます。そこでのループの順序は'row-wise'です。これを変更します。

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.

良くなったが、まだ非常に悪い。シングルループ - 良い。2重ループ - 悪い。MATLABはループのパフォーマンスを向上させるための適切な作業を行ったと思いますが、それでもループのオーバーヘッドは存在します。しかし、ループのオーバーヘッドは依然として存在しています。しかし、この計算はメモリバンド幅に制限されているので、ループのオーバーヘッドが見えてしまうのです。そして、あなたは そこで Func1 を呼び出すことによるオーバーヘッドがさらにはっきりと見えます。

では、arrayfunはどうなっているのでしょうか?そこにも関数はありませんし、多くのオーバーヘッドがあります。しかし、なぜ二重にネストされたループよりも悪いのでしょうか?実は、cellfun/arrayfunを使うというトピックは何度も広く議論されてきました(たとえば、以下のようなものです)。 ここで , ここ , ここ そして はこちら ). これらの関数は単純に遅いので、このような細かい計算には使えません。コードを簡潔にするためや、セルと配列の間の派手な変換をするためには使えます。しかし、その関数はあなたが書いたものよりも重くなければならない。

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

Soln7  0.016786 seconds.

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

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

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

for i=1:1000
   % compute
end

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

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

arrayfunはまだ悪いですが、少なくともベクトル化されたソリューションより3桁も悪いわけではないことがわかると思います。一方、列ごとの計算を行う単一のループは、完全にベクトル化されたバージョンと同じくらい高速です...。これはすべて1つのCPUで行われたものです。Soln5とSoln7の結果は、2コアに切り替えても変わりません。Soln5では、パーフォーを使って並列化する必要がありますね。高速化なんて忘れて...。Soln7はarrayfunが並列に動かないので、並列にはなりません。一方、Olisベクトル化版。

Oli  5.508085 seconds.