arrayfun può essere significativamente più lento di un ciclo esplicito in matlab. Perché?


105

Considera il seguente semplice test di velocità per 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

Sulla mia macchina (Matlab 2011b su Linux Mint 12), l'output di questo test è:

Elapsed time is 1.020689 seconds.
Elapsed time is 9.248388 seconds.

Che diavolo?!? arrayfun, sebbene sia certamente una soluzione dall'aspetto più pulito, è un ordine di grandezza più lento. Che cosa sta succedendo qui?

Inoltre, ho eseguito uno stile di test simile cellfune ho scoperto che è circa 3 volte più lento di un ciclo esplicito. Ancora una volta, questo risultato è l'opposto di quello che mi aspettavo.

La mia domanda è: perché arrayfune cellfuncosì tanto più lenti? E dato questo, ci sono buone ragioni per usarli (oltre a far sembrare il codice buono)?

Nota: sto parlando della versione standard di arrayfunqui, NON della versione GPU dal toolbox di elaborazione parallela.

EDIT: Giusto per essere chiari, sono consapevole che Func1sopra può essere vettorializzato come sottolineato da Oli. L'ho scelto solo perché fornisce un semplice test di velocità ai fini della domanda effettiva.

EDIT: Seguendo il suggerimento di grungetta, ho rifatto il test con feature accel off. I risultati sono:

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

In altre parole, sembrerebbe che una grande parte della differenza sia che l'acceleratore JIT fa un lavoro molto migliore di accelerare il forciclo esplicito di quanto non faccia arrayfun. Questo mi sembra strano, poiché arrayfunfornisce effettivamente più informazioni, ovvero il suo utilizzo rivela che l'ordine delle chiamate a Func1non ha importanza. Inoltre, ho notato che se l'acceleratore JIT è acceso o spento, il mio sistema utilizza sempre e solo una CPU ...


10
Fortunatamente, la "soluzione standard" rimane di gran lunga la più veloce: tic; 3 * x ^ 2 + 2 * x-1.; toc Il tempo trascorso è 0,030662 secondi.
Oli

4
@Oli Suppongo che avrei dovuto prevedere che qualcuno l'avrebbe fatto notare e avrebbe usato una funzione che non poteva essere vettorializzata :-)
Colin T Bowers,

3
Sarei interessato a vedere come cambia questo tempismo quando l'acceleratore JIT è spento. Esegui il comando "feature accel off" e quindi riesegui il test.
grungetta

@grungetta Suggerimento interessante. Ho aggiunto i risultati alla domanda insieme ad alcuni commenti.
Colin T Bowers,

Risposte:


101

Puoi avere l'idea eseguendo altre versioni del tuo codice. Considera la possibilità di scrivere esplicitamente i calcoli, invece di utilizzare una funzione nel tuo ciclo

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

È ora di calcolare sul mio computer:

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

Ora, mentre la soluzione completamente "vettorializzata" è chiaramente la più veloce, puoi vedere che la definizione di una funzione da chiamare per ogni voce x è un enorme sovraccarico. La semplice scrittura esplicita del calcolo ci ha fatto aumentare la velocità del fattore 5. Immagino che questo dimostri che il compilatore JIT di MATLAB non supporta le funzioni inline . Secondo la risposta di gnovice lì, in realtà è meglio scrivere una funzione normale piuttosto che anonima. Provalo.

Passaggio successivo: rimuovere (vettorializzare) il ciclo interno:

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.

Un altro fattore di accelerazione 5: c'è qualcosa in quelle affermazioni che dice che dovresti evitare i loop in MATLAB ... O c'è davvero? Dai un'occhiata a questo, allora

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.

Molto più vicino alla versione "completamente" vettorializzata. Matlab memorizza le matrici a livello di colonna. Dovresti sempre (quando possibile) strutturare i tuoi calcoli in modo che siano vettorializzati "a colonna".

Possiamo tornare a Soln3 ora. L'ordine del ciclo lì è "per riga". Consente di cambiarlo

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.

Meglio, ma comunque molto male. Single loop - buono. Doppio ciclo - cattivo. Immagino che MATLAB abbia fatto un lavoro decente per migliorare le prestazioni dei loop, ma l'overhead del loop è ancora presente. Se avessi un lavoro più pesante all'interno, non te ne accorgeresti. Ma poiché questo calcolo è limitato dalla larghezza di banda della memoria, si vede il sovraccarico del ciclo. E vedrai ancora più chiaramente il sovraccarico di chiamare Func1 lì.

Allora che succede con arrayfun? Nessuna funzione in linea neanche lì, quindi un sacco di overhead. Ma perché tanto peggio di un doppio ciclo annidato? In realtà, l'argomento dell'utilizzo di cellfun / arrayfun è stato ampiamente discusso molte volte (ad esempio qui , qui , qui e qui ). Queste funzioni sono semplicemente lente, non è possibile utilizzarle per calcoli a grana fine. Puoi usarli per brevità del codice e conversioni fantasiose tra celle e array. Ma la funzione deve essere più pesante di quella che hai scritto:

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

Soln7  0.016786 seconds.

Nota che Soln7 è una cella ora .. a volte è utile. Le prestazioni del codice ora sono abbastanza buone e se hai bisogno di celle come output, non è necessario convertire la matrice dopo aver utilizzato la soluzione completamente vettorizzata.

Allora perché arrayfun è più lento di una semplice struttura ad anello? Sfortunatamente, è impossibile per noi dirlo con certezza, poiché non è disponibile alcun codice sorgente. Puoi solo immaginare che poiché arrayfun è una funzione generica, che gestisce tutti i tipi di diverse strutture di dati e argomenti, non è necessariamente molto veloce in casi semplici, che puoi esprimere direttamente come nidi di loop. Da dove viene l'overhead non possiamo sapere. Il sovraccarico potrebbe essere evitato con una migliore implementazione? Forse no. Ma sfortunatamente l'unica cosa che possiamo fare è studiare la performance per identificare i casi in cui funziona bene e quelli in cui non funziona.

Aggiornamento Poiché il tempo di esecuzione di questo test è breve, per ottenere risultati affidabili ho aggiunto ora un ciclo attorno ai test:

for i=1:1000
   % compute
end

Alcune volte indicate di seguito:

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

Vedi che l'arrayfun è ancora cattivo, ma almeno non tre ordini di grandezza peggiore della soluzione vettorizzata. D'altra parte, un singolo ciclo con calcoli a colonna è veloce quanto la versione completamente vettorializzata ... Tutto questo è stato fatto su una singola CPU. I risultati per Soln5 e Soln7 non cambiano se passo a 2 core - In Soln5 dovrei usare un parfor per farlo parallelizzare. Dimentica la velocità ... Soln7 non funziona in parallelo perché arrayfun non funziona in parallelo. La versione vettorializzata di Olis invece:

Oli  5.508085 seconds.

9
Bella risposta! E i collegamenti a Matlab Central forniscono letture molto interessanti. Grazie molto.
Colin T Bowers,

Questa è una bella analisi.
H.Muster,

E un interessante aggiornamento! Questa risposta continua a dare :-)
Colin T Bowers,

3
solo un piccolo commento; indietro in MATLAB 6.5, è cellfunstato implementato come file MEX (con il codice sorgente C disponibile accanto). In realtà è stato abbastanza semplice. Ovviamente supportava solo l'applicazione di una delle 6 funzioni hard-coded (non era possibile passare un handle di funzione, solo una stringa con uno dei nomi di funzione)
Amro

1
arrayfun + handle di funzione = lento! evitarli in codice pesante.
Yvon

-8

Questo perché!!!!

x = randn(T, N); 

non è gpuarraytipo;

Tutto quello che devi fare è

x = randn(T, N,'gpuArray');

2
Penso che tu debba leggere la domanda e l'eccellente risposta di @angainor un po 'più attentamente. Non ha niente a che fare con gpuarray. Questo è quasi certamente il motivo per cui questa risposta è stata sottovalutata.
Colin T Bowers

@Colin - Sono d'accordo che angainor's è più completo, ma la risposta non menziona "gpuArray". Penso che "gpuArray" sia un buon contributo qui (se è corretto). Inoltre, la domanda è diventata un po 'sciatta con "Cosa sta succedendo qui?" , quindi penso che abbia aperto la porta a metodi aggiuntivi come la vettorizzazione dei dati e la spedizione a una GPU. Lascio correre questa risposta perché potrebbe aggiungere valore per i futuri visitatori. Mi scuso se ho fatto la chiamata sbagliata.
jww

1
Dimentichi anche il fatto che gpuarrayè supportato solo per le schede grafiche nVidia. Se non hanno tale hardware, il tuo consiglio (o la mancanza di) non ha senso. -1
rayryeng

D'altra parte, gpuarray è la spada laser della programmazione vettoriale matlab.
MrIO
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.