簡體   English   中英

通過空矩陣乘法初始化數組的更快方法? (Matlab)

[英]Faster way to initialize arrays via empty matrix multiplication? (Matlab)

在我看來,我偶然發現Matlab處理空矩陣的怪異方式。 例如,如果兩個空矩陣相乘,結果為:

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

vs:

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

兩者具有類double的1000x1000零矩陣的結果相同,但是空矩陣乘法1的速度快〜350倍! (使用tictoc以及循環會發生類似的結果)

怎么會這樣? timeittic,toc還是我發現了一種更快的初始化矩陣的方法? (這是使用matlab 2012a在Win7-64計算機intel-i5 650 3.2Ghz上完成的...)

編輯:

在閱讀您的反饋后,我更加仔細地研究了這種特性,並在2台不同的計算機(2012a相同的matlab版本)上進行了測試,該代碼檢查了運行時間與矩陣n的大小。 這是我得到的:

在此處輸入圖片說明

生成此使用的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>上圖所示的某個閾值矩陣大小,並獲得與空矩陣乘法(再次使用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)情況相似的結果。

這很奇怪,我看到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

編輯是否可以為f和g的末尾加上+1,並查看得到的時間。

編輯2013年1月6日,美國東部時間7:42

我正在遠程使用機器,因此對低質量的圖形感到抱歉(不得不將其生成為盲圖)。

機器配置:

i7920。2.653GHz。 Linux。 12 GB RAM。 8MB緩存。

在i7 920上生成的圖形

看起來甚至我可以訪問的機器都顯示了此行為,除了更大的尺寸(在1979年和2073年之間的某個地方)。 我沒有理由立即想到在更大尺寸下空矩陣乘法會更快。

回來之前,我將進行更多調查。

編輯2013年1月11日

在@EitanT的帖子之后,我想做更多的挖掘工作。 我編寫了一些C代碼,以了解matlab如何創建零矩陣。 這是我使用的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

因此,以下解釋了差異

  • zeros(..)在大尺寸時開始使用其他(慢速?)方案。
  • calloc行為也有些出乎意料,從而導致性能提高。

這是Linux上的行為。 有人可以在不同的機器(也許是不同的操作系統)上進行相同的實驗,看看該實驗是否成立嗎?

結果可能會產生誤導。 當您將兩個空矩陣相乘時,生成的矩陣不會立即“分配”和“初始化”,而是會推遲到您第一次使用它時(有點像惰性計算)。

索引超出范圍以增長變量時,情況也是如此,在數字數組的情況下,該變量將用零填充所有缺失的條目(我將在后面討論非數字的情況)。 當然,以這種方式生長矩陣不會覆蓋現有元素。

因此,雖然看起來更快,但是您只是在延遲分配時間,直到您真正開始使用矩陣。 最后,您將擁有與開始時一樣的分配時間。

其他幾種選擇相比,展示此行為的示例:

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函數比運行腳本或在命令提示符下運行更快,因為某些優化只會以這種方式發生...

無論哪種方式,分配通常都會執行一次,因此誰在乎是否需要額外的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 (使用Dependency Walker工具)導出的C函數的列表,發現了一些有趣的東西。

  • 有一些未記錄的函數可以創建未初始化的數組: mxCreateUninitDoubleMatrixmxCreateUninitNumericArraymxCreateUninitNumericMatrix 實際上, 文件交換上有一個提交使用這些功能來提供更快的替代zeros功能的方式。

  • 存在一個未記錄的函數,稱為mxFastZeros 在線上進行谷歌搜索,我還可以看到您也將此問題交叉發布在MATLAB Answers上,那里有一些出色的答案。 James Tursa(之前是UNINIT的同一作者)舉了一個有關如何使用此未記錄功能的示例

  • libmx.dlltbbmalloc.dll共享庫鏈接。 這是Intel TBB可擴展內存分配器。 該庫提供了等效的內存分配功能( malloccallocfree ),這些功能針對並行應用程序進行了優化。 請記住,許多MATLAB函數是自動多線程的 ,因此,如果矩陣大小足夠大,如果zeros(..)是多線程的並且使用Intel的內存分配器,我不會感到驚訝(這是Loren Shure的最新評論證實了這一事實) 。

關於內存分配器的最后一點,您可以在C / C ++中編寫類似於@PavanYalamanchili的基准,並比較可用的各種分配器。 這樣的東西。 請記住,MEX文件的內存管理開銷略高,因為MATLAB使用mxCallocmxMallocmxRealloc函數自動釋放在MEX文件中分配的所有內存。 值得一試的是,以前可以在舊版本中更改內部內存管理器


編輯:

這是一個比較詳盡的基准,用於比較討論的替代方案。 它具體表明,一旦強調使用整個分配的矩陣,所有這三種方法都處於平等地位,並且差異可以忽略不計。

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

經過研究之后,我在“ Undocumented Matlab”中找到了這篇文章Yair Altman先生已經得出結論, MathWork使用zeros(M, N) 預分配矩陣的方法的確不是最有效的方法。

他給x = zeros(M,N)clear x, x(M,N) = 0計時,發現后者快了約500倍。 根據他的解釋,第二種方法只是創建一個M×N矩陣,其元素會自動初始化為0。但是,第一種方法會創建x (其中x具有自動零元素),然后為每個矩陣分配一個零。再次在x元素,這是一個多余的操作,需要花費更多時間。

對於空矩陣乘法,例如您在問題中所顯示的,MATLAB期望乘積為M×N矩陣,因此將分配一個M×N矩陣。 因此,輸出矩陣會自動初始化為零。 由於原始矩陣為空,因此不執行進一步的計算,因此輸出矩陣中的元素保持不變並等於零。

有趣的問題是,顯然有幾種方法可以擊敗內置的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.

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM