64

私は、Matlab が空の行列を扱っているという奇妙な方法 (私の見解では) に出くわしました。たとえば、2 つの空行列を乗算すると、結果は次のようになります。

zeros(3,0)*zeros(0,3)
ans =

 0     0     0
 0     0     0
 0     0     0

さて、これはすでに私を驚かせましたが、簡単な検索で上記のリンクにたどり着き、なぜこれが起こっているのかについてのややねじれた論理の説明を得ました.

しかし、次の観察に備えるものは何もありませんでした。zeros(n)このタイプの乗算は、たとえば初期化の目的で関数を使用する場合と比較して、どの程度効率的であるかを自問しました。私はこれtimeitに答えていました:

f=@() zeros(1000)
timeit(f)
ans =
    0.0033

対:

g=@() zeros(1000,0)*zeros(0,1000)
timeit(g)
ans =
    9.2048e-06

どちらも、クラス のゼロの 1000x1000 行列の結果は同じですdoubleが、空行列の乗算の方が ~350 倍高速です! tic( andtocとループを使用しても同様の結果が得られます)

どうすればいいの?ブラフしているtimeittic,toc、マトリックスを初期化するためのより高速な方法を見つけましたか? (これは、win7-64 マシン、intel-i5 650 3.2Ghz 上の matlab 2012a で行われました...)

編集:

あなたのフィードバックを読んだ後、私はこの特異性をより注意深く調べ、実行時間と行列 n のサイズを調べるコードを 2 台の異なるコンピューター (2012a でも同じ matlab バージョン) でテストしました。これは私が得るものです:

ここに画像の説明を入力

これを生成するコードは以前と同じように使用されますが、 andを使用timeitしたループは同じように見えます。したがって、小さいサイズの場合は同等です。ただし、空行列の乗算ではパフォーマンスが飛躍的に向上します。そのプロットを生成するために使用したコードは次のとおりです。tictoczeros(n)n=400

n=unique(round(logspace(0,4,200)));
for k=1:length(n)
    f=@() zeros(n(k));
    t1(k)=timeit(f);

    g=@() zeros(n(k),0)*zeros(0,n(k));
    t2(k)=timeit(g);
end

loglog(n,t1,'b',n,t2,'r');
legend('zeros(n)','zeros(n,0)*zeros(0,n)',2);
xlabel('matrix size (n)'); ylabel('time [sec]');

これも経験した方いますか?

編集#2:

ちなみに、この効果を得るために空行列の乗算は必要ありません。簡単にできます:

z(n,n)=0;

ここで、n> 前のグラフに見られるしきい値行列サイズであり、空行列の乗算と同様に正確な効率プロファイルを取得します (再び timeit を使用)。

ここに画像の説明を入力

コードの効率を向上させる例を次に示します。

n = 1e4;
clear z1
tic
z1 = zeros( n ); 
for cc = 1 : n
    z1(:,cc)=cc;
end
toc % Elapsed time is 0.445780 seconds.

%%
clear z0
tic
z0 = zeros(n,0)*zeros(0,n);
for cc = 1 : n
    z0(:,cc)=cc;
end
toc % Elapsed time is 0.297953 seconds.

ただし、z(n,n)=0;代わりに使用すると、zeros(n)ケースと同様の結果が得られます。

4

4 に答える 4

33

これは奇妙です。f は速く、g はあなたが見ているものよりも遅いのです。しかし、どちらも私にとっては同じです。おそらくMATLABの異なるバージョンですか?

>> g = @() zeros(1000, 0) * zeros(0, 1000);
>> f = @() zeros(1000)
f =     
    @()zeros(1000)
>> timeit(f)  
ans =    
   8.5019e-04
>> timeit(f)  
ans =    
   8.4627e-04
>> timeit(g)  
ans =    
   8.4627e-04

EDIT f と g の末尾に + 1 を追加して、取得している時間を確認できます。

編集 2013 年 1 月 6 日 7:42 EST

私はマシンをリモートで使用しているため、グラフの品質が低くて申し訳ありません (ブラインドで生成する必要がありました)。

マシン構成:

i7 920。2.653 GHz。Linux。12 GB の RAM。8MBのキャッシュ。

i7 920 で生成されたグラフ

私がアクセスできるマシンでさえ、より大きなサイズ(1979年から2073年の間)を除いて、この動作を示しているようです。空行列の乗算が大きなサイズで高速になる理由は今のところ考えられません。

戻る前にもう少し調べてみます。

編集 2013 年 1 月 11 日

@EitanT の投稿の後、もう少し掘り下げたいと思いました。matlab がどのようにゼロ行列を作成しているかを確認するために、いくつかの C コードを書きました。これが私が使用したc++コードです。

int main(int argc, char **argv)
{
    for (int i = 1975; i <= 2100; i+=25) {
    timer::start();
    double *foo = (double *)malloc(i * i * sizeof(double));
    for (int k = 0; k < i * i; k++) foo[k]  = 0;
    double mftime = timer::stop();
    free(foo);

    timer::start();
    double *bar = (double *)malloc(i * i * sizeof(double));
    memset(bar, 0, i * i * sizeof(double));
    double mmtime = timer::stop();
    free(bar);

    timer::start();
    double *baz = (double *)calloc(i * i, sizeof(double));
    double catime = timer::stop();
    free(baz);

    printf("%d, %lf, %lf, %lf\n", i, mftime, mmtime, catime);
    }
}

これが結果です。

$ ./test
1975, 0.013812, 0.013578, 0.003321
2000, 0.014144, 0.013879, 0.003408
2025, 0.014396, 0.014219, 0.003490
2050, 0.014732, 0.013784, 0.000043
2075, 0.015022, 0.014122, 0.000045
2100, 0.014606, 0.014480, 0.000045

ご覧のとおりcalloc(4 列目) が最速の方法のようです。また、2025年から2050年の間に大幅に高速化しています(2048年頃になると思いますか?)。

今、同じことを確認するためにmatlabに戻りました。これが結果です。

>> test
1975, 0.003296, 0.003297
2000, 0.003377, 0.003385
2025, 0.003465, 0.003464
2050, 0.015987, 0.000019
2075, 0.016373, 0.000019
2100, 0.016762, 0.000020

f() と g() の両方がcallocより小さいサイズ (<2048 ?) で使用されているようです。しかし、より大きなサイズでは、 f() (zeros(m, n)) はmalloc+を使用し始めますがmemset、 g() (zeros(m, 0) * zeros(0, n)) は を使用し続けcallocます。

したがって、発散は次のように説明されます

  • zeros(..) は、より大きなサイズで別の (遅い ?) スキームを使用し始めます。
  • callocまた、パフォーマンスの向上につながる、やや予期しない動作をします。

これは Linux での動作です。誰かが別のマシン (およびおそらく別の OS) で同じ実験を行い、実験が成り立つかどうかを確認できますか?

于 2013-01-05T07:05:41.477 に答える
30

結果は少し誤解を招く可能性があります。2 つの空の行列を乗算すると、結果の行列はすぐに「割り当て」および「初期化」されず、最初に使用するまで延期されます (遅延評価のようなもの)。

範囲外にインデックスを付けて変数を大きくする場合も同じことが当てはまります。数値配列の場合、欠落しているエントリはゼロで埋められます (非数値の場合については後で説明します)。もちろん、この方法で行列を成長させても、既存の要素は上書きされません。

そのため、高速に見えるかもしれませんが、実際にマトリックスを最初に使用するまで割り当て時間を遅らせているだけです。最終的には、最初から割り当てを行った場合と同様のタイミングになります。

他のいくつかの選択肢と比較して、この動作を示す例:

N = 1000;

clear z
tic, z = zeros(N,N); toc
tic, z = z + 1; toc
assert(isequal(z,ones(N)))

clear z
tic, z = zeros(N,0)*zeros(0,N); toc
tic, z = z + 1; toc
assert(isequal(z,ones(N)))

clear z
tic, z(N,N) = 0; toc
tic, z = z + 1; toc
assert(isequal(z,ones(N)))

clear z
tic, z = full(spalloc(N,N,0)); toc
tic, z = z + 1; toc
assert(isequal(z,ones(N)))

clear z
tic, z(1:N,1:N) = 0; toc
tic, z = z + 1; toc
assert(isequal(z,ones(N)))

clear z
val = 0;
tic, z = val(ones(N)); toc
tic, z = z + 1; toc
assert(isequal(z,ones(N)))

clear z
tic, z = repmat(0, [N N]); toc
tic, z = z + 1; toc
assert(isequal(z,ones(N)))

結果は、それぞれのケースで両方の命令の経過時間を合計すると、同様の合計タイミングになることを示しています。

// zeros(N,N)
Elapsed time is 0.004525 seconds.
Elapsed time is 0.000792 seconds.

// zeros(N,0)*zeros(0,N)
Elapsed time is 0.000052 seconds.
Elapsed time is 0.004365 seconds.

// z(N,N) = 0
Elapsed time is 0.000053 seconds.
Elapsed time is 0.004119 seconds.

他のタイミングは次のとおりです。

// full(spalloc(N,N,0))
Elapsed time is 0.001463 seconds.
Elapsed time is 0.003751 seconds.

// z(1:N,1:N) = 0
Elapsed time is 0.006820 seconds.
Elapsed time is 0.000647 seconds.

// val(ones(N))
Elapsed time is 0.034880 seconds.
Elapsed time is 0.000911 seconds.

// repmat(0, [N N])
Elapsed time is 0.001320 seconds.
Elapsed time is 0.003749 seconds.

これらの測定値はミリ秒単位では小さすぎて、あまり正確ではない可能性があるため、これらのコマンドをループで数千回実行して平均を取ることができます。また、保存された M 関数を実行すると、スクリプトやコマンド プロンプトを実行するよりも高速になることがあります。

どちらの方法でも、割り当ては通常 1 回行われるため、さらに 30 ミリ秒かかるかどうかは誰が気にします :)


セル配列または構造体の配列でも同様の動作が見られます。次の例を検討してください。

N = 1000;

tic, a = cell(N,N); toc
tic, b = repmat({[]}, [N,N]); toc
tic, c{N,N} = []; toc

与える:

Elapsed time is 0.001245 seconds.
Elapsed time is 0.040698 seconds.
Elapsed time is 0.004846 seconds.

それらがすべて等しい場合でも、それらは異なる量のメモリを占有することに注意してください。

>> assert(isequal(a,b,c))
>> whos a b c
  Name         Size                  Bytes  Class    Attributes

  a         1000x1000              8000000  cell               
  b         1000x1000            112000000  cell               
  c         1000x1000              8000104  cell               

実際、MATLAB はおそらく複数のコピーを作成するのではなく、すべてのセルに対して同じ空行列を共有しているため、状況はここではもう少し複雑です。

セル配列aは、実際には初期化されていないセルの配列 (NULL ポインターの配列) であり、一方、bは各セルが空の配列であるセル配列です[](内部的にも、データ共有のために、最初のセルのみがb{1}指し[]、残りはすべて最初のセルへの参照)。最後の配列は(初期化されていないセル) にc似ていますが、最後の配列には空の数値行列が含まれています。a[]


libmx.dll( Dependency Walkerツールを使用して)からエクスポートされた C 関数のリストを調べたところ、興味深いことがいくつか見つかりました。

  • 初期化されていない配列を作成するための文書化されていない関数があります: mxCreateUninitDoubleMatrixmxCreateUninitNumericArray、およびmxCreateUninitNumericMatrix。実際、File Exchangeでは、これらの機能を利用して、より高速な代替機能を提供していzerosます。

  • という文書化されていない関数が存在しますmxFastZeros。オンラインでグーグルすると、この質問が MATLAB Answers にもクロス投稿されていることがわかります。そこにはいくつかの優れた回答があります。James Tursa (以前の UNINIT の作者と同じ) は、この文書化されていない関数の使用方法の例を示しました。

  • libmx.dlltbbmalloc.dll共有ライブラリにリンクされています。これはIntel TBBスケーラブル メモリ アロケータです。このライブラリは、並列アプリケーション用に最適化された同等のメモリ割り当て関数 ( malloccalloc、 ) を提供します。free多くの MATLAB 関数は自動的にマルチスレッド化zeros(..)されるため、がマルチスレッド化され、行列のサイズが十分に大きくなれば Intel のメモリ アロケータを使用しても驚かないでしょう(これは、この事実を確認するLoren Shureによる最近のコメントです)。

メモリ アロケータに関する最後のポイントについては、 @PavanYalamanchiliと同様のベンチマークを C/C++ で作成し、使用可能なさまざまなアロケータを比較できます。このようなもの。MATLAB は、、または関数を使用して MEX ファイルに割り当てられたメモリを自動的に解放するため、 MEX ファイルのメモリ管理オーバーヘッドはわずかに高くなります。価値があるのは、以前のバージョンでは内部メモリ マネージャーを変更することが可能だったことです。mxCallocmxMallocmxRealloc


編集:

これは、議論された代替案を比較するためのより完全なベンチマークです。割り当てられたマトリックス全体の使用を強調すると、3 つの方法はすべて同等の立場にあり、違いはごくわずかであることを具体的に示しています。

function compare_zeros_init()
    iter = 100;
    for N = 512.*(1:8)
        % ZEROS(N,N)
        t = zeros(iter,3);
        for i=1:iter
            clear z
            tic, z = zeros(N,N); t(i,1) = toc;
            tic, z(:) = 9; t(i,2) = toc;
            tic, z = z + 1; t(i,3) = toc;
        end
        fprintf('N = %4d, ZEROS = %.9f\n', N, mean(sum(t,2)))

        % z(N,N)=0
        t = zeros(iter,3);
        for i=1:iter
            clear z
            tic, z(N,N) = 0; t(i,1) = toc;
            tic, z(:) = 9; t(i,2) = toc;
            tic, z = z + 1; t(i,3) = toc;
        end
        fprintf('N = %4d, GROW  = %.9f\n', N, mean(sum(t,2)))

        % ZEROS(N,0)*ZEROS(0,N)
        t = zeros(iter,3);
        for i=1:iter
            clear z
            tic, z = zeros(N,0)*zeros(0,N); t(i,1) = toc;
            tic, z(:) = 9; t(i,2) = toc;
            tic, z = z + 1; t(i,3) = toc;
        end
        fprintf('N = %4d, MULT  = %.9f\n\n', N, mean(sum(t,2)))
    end
end

以下は、行列サイズの増加に関して、100回の反復で平均化されたタイミングです。R2013a でテストを実行しました。

>> compare_zeros_init
N =  512, ZEROS = 0.001560168
N =  512, GROW  = 0.001479991
N =  512, MULT  = 0.001457031

N = 1024, ZEROS = 0.005744873
N = 1024, GROW  = 0.005352638
N = 1024, MULT  = 0.005359236

N = 1536, ZEROS = 0.011950846
N = 1536, GROW  = 0.009051589
N = 1536, MULT  = 0.008418878

N = 2048, ZEROS = 0.012154002
N = 2048, GROW  = 0.010996315
N = 2048, MULT  = 0.011002169

N = 2560, ZEROS = 0.017940950
N = 2560, GROW  = 0.017641046
N = 2560, MULT  = 0.017640323

N = 3072, ZEROS = 0.025657999
N = 3072, GROW  = 0.025836506
N = 3072, MULT  = 0.051533432

N = 3584, ZEROS = 0.074739924
N = 3584, GROW  = 0.070486857
N = 3584, MULT  = 0.072822335

N = 4096, ZEROS = 0.098791732
N = 4096, GROW  = 0.095849788
N = 4096, MULT  = 0.102148452
于 2013-08-13T19:51:40.230 に答える
27

いくつかの調査を行った後、「Undocumented Matlab」でこの記事を見つけました。Yair Altman、MathWorkを使用して行列を事前に割り当てる方法は実際には最も効率的な方法ではないという結論にすでに達していました。zeros(M, N)

彼はx = zeros(M,N)vs.の時間を測定clear x, x(M,N) = 0し、後者のほうが約 500 倍速いことを発見しました。彼の説明によると、2 番目の方法は単純に M 行 N 列の行列を作成し、その要素は自動的に 0 に初期化されます。ただし、最初の方法はx(x自動ゼロ要素を使用して) を作成し、すべての要素にゼロを割り当てます。x繰り返しますが、これはより時間がかかる冗長な操作です。

質問で示したような空の行列乗算の場合、MATLAB は積が M×N 行列であると想定するため、M×N 行列を割り当てます。したがって、出力行列は自動​​的にゼロに初期化されます。元の行列は空であるため、それ以上の計算は実行されず、出力行列の要素は変更されず、ゼロのままです。

于 2013-01-07T11:48:08.223 に答える
2

zeros興味深い質問ですが、組み込み関数を「打ち負かす」には明らかにいくつかの方法があります。なぜこれが起こっているのかについての私の唯一の推測は、それがよりメモリ効率が良い(結局のところ、zeros(LargeNumer)ほとんどのコードで破壊的な速度のボトルネックを形成するよりも早くMatlabがメモリ制限に達する原因になる)、または何らかの形でより堅牢になる可能性があるということです。

これは、スパース行列を使用する別の高速割り当て方法です。ベンチマークとして通常の零点関数を追加しました。

tic; x=zeros(1000,1000); toc
Elapsed time is 0.002863 seconds.

tic; clear x; x(1000,1000)=0; toc
Elapsed time is 0.000282 seconds.

tic; x=full(spalloc(1000,1000,0)); toc
Elapsed time is 0.000273 seconds.

tic; x=spalloc(1000,1000,1000000); toc %Is this the same for practical purposes?
Elapsed time is 0.000281 seconds.
于 2013-01-11T10:23:44.067 に答える