web-dev-qa-db-fra.com

arrayfun peut être significativement plus lent qu'une boucle explicite dans matlab. Pourquoi?

Considérez le test de vitesse simple suivant pour 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

Sur ma machine (Matlab 2011b sur Linux Mint 12), la sortie de ce test est:

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

Qu'est-ce que?!? arrayfun, tout en étant certes une solution d'apparence plus propre, est un ordre de grandeur plus lent. Qu'est-ce qui se passe ici?

De plus, j'ai fait un style de test similaire pour cellfun et je l'ai trouvé environ 3 fois plus lent qu'une boucle explicite. Encore une fois, ce résultat est l'opposé de ce que j'attendais.

Ma question est: Pourquoi arrayfun et cellfun sont-ils tellement plus lents? Et étant donné cela, y a-t-il de bonnes raisons de les utiliser (autres que pour rendre le code beau)?

Remarque: Je parle ici de la version standard de arrayfun, PAS de la version GPU de la boîte à outils de traitement parallèle.

EDIT: Juste pour être clair, je sais que Func1 ci-dessus peut être vectorisé comme l'a souligné Oli. Je ne l'ai choisi que parce qu'il donne un simple test de vitesse aux fins de la question réelle.

EDIT: Suite à la suggestion de grungetta, j'ai refait le test avec feature accel off. Les résultats sont:

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

En d'autres termes, il semblerait qu'une grande partie de la différence est que l'accélérateur JIT fait un bien meilleur travail d'accélération de la boucle explicite for que arrayfun. Cela me semble étrange, car arrayfun fournit en fait plus d'informations, c'est-à-dire que son utilisation révèle que l'ordre des appels à Func1 n'a pas d'importance. De plus, j'ai remarqué que si l'accélérateur JIT est allumé ou éteint, mon système n'utilise qu'un seul processeur ...

101
Colin T Bowers

Vous pouvez vous faire une idée en exécutant d'autres versions de votre code. Envisagez d'écrire explicitement les calculs, au lieu d'utiliser une fonction dans votre boucle

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

Il est temps de calculer sur mon ordinateur:

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

Maintenant, alors que la solution entièrement "vectorisée" est clairement la plus rapide, vous pouvez voir que la définition d'une fonction à appeler pour chaque entrée x est un énorme surcharge . L'écriture explicite du calcul nous a permis d'accélérer le facteur 5. Je suppose que cela montre que le compilateur MATLABs JIT ne prend pas en charge les fonctions en ligne . Selon la réponse de gnovice, il est en fait préférable d'écrire une fonction normale plutôt qu'une fonction anonyme. Essayez-le.

Étape suivante - supprimer (vectoriser) la boucle intérieure:

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.

Autre accélération du facteur 5: il y a quelque chose dans ces déclarations qui dit que vous devriez éviter les boucles dans MATLAB ... Ou est-ce vraiment? Jetez un oeil à cela alors

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.

Beaucoup plus proche de la version vectorisée "entièrement". Matlab stocke les matrices par colonne. Vous devez toujours (si possible) structurer vos calculs pour qu'ils soient vectorisés "colonne par colonne".

Nous pouvons revenir à Soln3 maintenant. L'ordre des boucles y est "par ligne". Permet de le changer

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.

Mieux, mais toujours très mauvais. Boucle unique - bon. Double boucle - mauvais. Je suppose que MATLAB a fait un travail décent pour améliorer les performances des boucles, mais la surcharge de boucle est toujours là. Si vous aviez un travail plus lourd à l'intérieur, vous ne le remarqueriez pas. Mais comme ce calcul est limité à la bande passante mémoire, vous voyez la surcharge de la boucle. Et vous verrez encore plus clairement le surcoût d'appeler Func1 là-bas.

Alors quoi de neuf avec arrayfun? Pas de fonction non plus là-bas, donc beaucoup de frais généraux. Mais pourquoi tant pire qu'une boucle double imbriquée? En fait, le sujet de l'utilisation de cellfun/arrayfun a été largement discuté à plusieurs reprises (par exemple ici , ici , ici et ici) ). Ces fonctions sont simplement lentes, vous ne pouvez pas les utiliser pour de tels calculs à grain fin. Vous pouvez les utiliser pour la brièveté du code et les conversions fantaisistes entre les cellules et les tableaux. Mais la fonction doit être plus lourde que ce que vous avez écrit:

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

Soln7  0.016786 seconds.

Notez que Soln7 est une cellule maintenant .. parfois c'est utile. Les performances du code sont assez bonnes maintenant, et si vous avez besoin d'une cellule en sortie, vous n'avez pas besoin de convertir votre matrice après avoir utilisé la solution entièrement vectorisée.

Alors pourquoi arrayfun est-il plus lent qu'une simple structure en boucle? Malheureusement, il nous est impossible de le dire avec certitude, car aucun code source n'est disponible. Vous ne pouvez que deviner que puisque arrayfun est une fonction à usage général, qui gère toutes sortes de structures de données et d'arguments différents, elle n'est pas nécessairement très rapide dans les cas simples, que vous pouvez exprimer directement sous forme de nids de boucle. D'où viennent les frais généraux, nous ne pouvons pas le savoir. Les frais généraux pourraient-ils être évités par une meilleure mise en œuvre? Peut être pas. Mais malheureusement, la seule chose que nous pouvons faire est d'étudier la performance pour identifier les cas, dans lesquels cela fonctionne bien, et ceux, où cela ne fonctionne pas.

Mise à jour Comme le temps d'exécution de ce test est court, pour obtenir des résultats fiables, j'ai maintenant ajouté une boucle autour des tests:

for i=1:1000
   % compute
end

Quelques fois ci-dessous:

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

Vous voyez que le arrayfun est toujours mauvais, mais au moins pas trois ordres de grandeur pire que la solution vectorisée. En revanche, une seule boucle avec des calculs colonne par colonne est aussi rapide que la version entièrement vectorisée ... Tout cela a été fait sur un seul processeur. Les résultats pour Soln5 et Soln7 ne changent pas si je passe à 2 cœurs - Dans Soln5, je devrais utiliser un parfor pour le mettre en parallèle. Oubliez l'accélération ... Soln7 ne s'exécute pas en parallèle car arrayfun ne s'exécute pas en parallèle. Version vectorisée Olis en revanche:

Oli  5.508085 seconds.
101
angainor