web-dev-qa-db-ja.com

空行列の乗算を介して配列を初期化するより速い方法は? (Matlab)

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
_

どちらもクラスdoubleのゼロの1000x1000行列の同じ結果をもたらしますが、空行列の乗算は約350倍高速です。 (同様の結果は、tictocおよびループを使用して発生します)

どうすればいいの? timeitまたは_tic,toc_ブラフですか、それとも行列を初期化するより速い方法を見つけましたか? (これは、win7-64マシン、intel-i5 6503.2Ghzでmatlab2012aを使用して行われました...)

編集:

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

enter image description here

これを生成するコードは以前と同じようにtimeitを使用しましたが、tictocのループは同じように見えます。したがって、小さいサイズの場合、zeros(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>前のグラフに見られるしきい値行列サイズであり、空行列乗算の場合と同様にexact効率プロファイルを取得します(ここでもtimeitを使用)。

enter image description here

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

_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)の場合と同様の結果が得られます。

63
bla

これは奇妙なことです。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

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

マシン構成:

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

Graph generated on 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()の両方が小さいサイズ(<2048?)でcallocを使用しているようですが、大きいサイズではf()(zeros(m、n))はmalloc + memsetの使用を開始し、g() (zeros(m、0)* zeros(0、n))はcallocを使い続けます。

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

  • ゼロ(..)は、より大きなサイズで異なる(より遅い?)スキームを使用し始めます。
  • callocも多少予期しない動作をするため、パフォーマンスが向上します。

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

33

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

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

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

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

_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}_は_[]_を指し、残りはすべて最初のセルへの参照を持ちます)。最終的な配列ca(初期化されていないセル)に似ていますが、最後の配列には空の数値行列_[]_が含まれています。


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

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

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

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

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


編集:

これは、議論された代替案を比較するためのより徹底的なベンチマークです。具体的には、割り当てられたマトリックス全体の使用を強調すると、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
_
29
Amro

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

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

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

27
Eitan T

興味深い質問ですが、組み込みの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.
2