2010-03-20 9 views
6

J'ai fait une petite expérience comme cela sera montré ci-dessous et il me semble qu'une boucle while est plus rapide qu'une boucle for en Perl. Mais puisque l'expérience était plutôt grossière, et le sujet pourrait être beaucoup plus compliqué qu'il n'y paraît, j'aimerais entendre ce que vous avez à dire à ce sujet. Merci pour les commentaires/suggestions :)En Perl, une boucle while est-elle généralement plus rapide qu'une boucle for?

Dans les deux petits scripts suivants, j'ai essayé séparément et pendant des boucles de calculer la factorielle de 100 000. Celui qui a la boucle while a pris 57 minutes 17 secondes pour terminer alors que l'équivalent pour boucle for a pris 1 heure 7 minutes 54 secondes.

Script qui a while:

use strict; 
use warnings; 
use bigint; 

my $now = time; 

my $n = shift; 
my $s = 1; 

while(1){ 
$s *= $n; 
$n--; 
last if $n==2; 
} 

print $s*$n; 
$now = time - $now; 
printf("\n\nTotal running time: %02d:%02d:%02d\n\n", int($now/3600), 
      int(($now % 3600)/60), int($now % 60)); 

Script qui a pour la boucle:

use strict; 
use warnings; 
use bigint; 

my $now = time; 

my $n =shift; 
my $s=1; 

for (my $i=2; $i<=$n;$i++) { 
$s = $s*$i; 
} 

print $s; 
$now = time - $now; 
printf("\n\nTotal running time: %02d:%02d:%02d\n\n", int($now/3600), 
      int(($now % 3600)/60), int($now % 60)); 
+3

Vous essayez probablement d'optimiser la mauvaise chose. Je me demande pourquoi vous pensez que cette partie particulière de la langue est si importante? – Ether

+0

Exécutez-les à plusieurs reprises, et ils auront probablement la moyenne à peu près au même – CaffGeek

+0

@Chad, en fait j'ai déjà testé le code à plusieurs reprises. Ils ont pris différents temps pour terminer le même travail. Je pense que l'explication de Jonathan Leffler avec le code d'illustration est très logique. – Mike

Répondre

8

Les boucles ne sont pas équivalentes, et vous écrasez principalement le paquetage bigint et cela n'a rien à voir avec for par rapport à while en soi.

La boucle while utilise la notation '$s *= $i' mais la boucle for utilise '$s = $s * $i'. Il est assez simple de démontrer que ceux-ci ne sont pas identiques. En outre, une boucle compte; l'autre compte à rebours. Cela affecte la taille des nombres à multiplier. C'est un effet de second ordre - mais pas complètement négligeable.

[Mise à jour: mise à jour pour afficher une seule version du code, avec des sous-secondes. Il y a de la place pour penser que l'impression devrait être exclue des calculs de synchronisation; Cela rend les choses plus désordonnées, donc je n'ai pas dérangé. J'ai corrigé le bug dans la version précédente: la boucle 4 était la même que la boucle 3 - maintenant ce n'est pas le cas. J'ai également mis au point la mise en forme de la sortie (bien que la gestion des sous-secondes puisse être améliorée - un exercice pour le lecteur), et il existe de meilleurs rapports sur les progrès.]

Les résultats de synchronisation sur un Mac Mini (Snow Leopard 10.6.2) étaient les suivants:

Count up $s *= $i:  00:00:12.663337 
Count up $s = $s * $i: 00:00:20.686111 
Count down $s *= $i:  00:00:14.201797 
Count down $s = $s * $i: 00:00:23.269874 

Le script:

use Time::HiRes qw(gettimeofday); 
use strict; 
use warnings; 
use bigint; 
use constant factorial_of => 13000; 

sub delta_t 
{ 
    my($tag, $t1, $t2) = @_; 
    my($d) = int($t2 - $t1); 
    my($f) = ($t2 - $t1) - $d; 
    my($s) = sprintf("%.6f", $f); 
    $s =~ s/^0//; 
    printf "%-25s %02d:%02d:%02d%s\n", 
      $tag, int($d/3600), int(($d % 3600)/60), int($d % 60), $s; 
} 

my $t1 = gettimeofday; 

{ 
    my $n = factorial_of; 
    my $s = 1; 
    for (my $i = 2; $i <= $n; $i++) 
    { 
     $s *= $i; 
    } 
    print "$s\n: Loop 1\n"; 
} 

my $t2 = gettimeofday; 
delta_t('Count up $s *= $i:',  $t1, $t2); 

{ 
    my $n = factorial_of; 
    my $s = 1; 
    for (my $i = 2; $i <= $n; $i++) 
    { 
     $s = $s * $i; 
    } 
    print "$s\n: Loop 2\n"; 
} 

my $t3 = gettimeofday; 
delta_t('Count up $s *= $i:',  $t1, $t2); 
delta_t('Count up $s = $s * $i:', $t2, $t3); 

{ 
    my $n = factorial_of; 
    my $s = 1; 
    for (my $i = $n; $i > 1; $i--) 
    { 
     $s *= $i; 
    } 
    print "$s\n: Loop 3\n"; 
} 

my $t4 = gettimeofday; 
delta_t('Count up $s *= $i:',  $t1, $t2); 
delta_t('Count up $s = $s * $i:', $t2, $t3); 
delta_t('Count down $s *= $i:',  $t3, $t4); 

{ 
    my $n = factorial_of; 
    my $s = 1; 
    for (my $i = $n; $i > 1; $i--) 
    { 
     $s = $s * $i; 
    } 
    print "$s\n: Loop 4\n"; 
} 

my $t5 = gettimeofday; 
delta_t('Count up $s *= $i:',  $t1, $t2); 
delta_t('Count up $s = $s * $i:', $t2, $t3); 
delta_t('Count down $s *= $i:',  $t3, $t4); 
delta_t('Count down $s = $s * $i:', $t4, $t5); 

Et voici une version beaucoup plus compacte le code ci-dessus, étendu pour tester 'while' boucles ainsi que 'for' boucles. Il traite également de la plupart des problèmes de synchronisation. La seule chose qui n'est pas idéale (pour moi) est qu'elle utilise un couple de variables globales, et j'ai légèrement modifié le code dans le code refs pour que tout se place sur une ligne sans déclencher de barre de défilement (sur mon écran, de toute façon). Clairement, avec un peu plus de travail, le test pourrait être enveloppé dans un tableau, de sorte que le test serait fait de manière itérative - une boucle dans le tableau exécutant la fonction timer sur les informations dans le tableau. Etc...c'est un SMOP - simple question de programmation. (Il imprime le hachage MD5 de la factorielle, plutôt que la factorielle elle-même, car il est plus facile de comparer les résultats, etc ... Il a fait ressortir quelques erreurs alors que je refaisais le code ci-dessus: Oui, MD5 n'est pas sécurisé - mais je ne l'utilise pas pour la sécurité, il suffit de repérer les changements involontaires)

use Time::HiRes qw(gettimeofday); 
use Digest::MD5 qw(md5_hex); 
use strict; 
use warnings; 
use bigint; 
use constant factorial_of => 13000; 

my ($s, $i); 

my $l1 = sub {my($n) = @_; for ($i = 2; $i <= $n; $i++) { $s *= $i;  }}; 
my $l2 = sub {my($n) = @_; for ($i = 2; $i <= $n; $i++) { $s = $s * $i; }}; 
my $l3 = sub {my($n) = @_; for ($i = $n; $i > 1; $i--) { $s *= $i;  }}; 
my $l4 = sub {my($n) = @_; for ($i = $n; $i > 1; $i--) { $s = $s * $i; }}; 
my $l5 = sub {my($n) = @_; $i = 2; while ($i <= $n) { $s *= $i;  $i++; }}; 
my $l6 = sub {my($n) = @_; $i = 2; while ($i <= $n) { $s = $s * $i; $i++; }}; 
my $l7 = sub {my($n) = @_; $i = $n; while ($i > 1) { $s *= $i;  $i--; }}; 
my $l8 = sub {my($n) = @_; $i = $n; while ($i > 1) { $s = $s * $i; $i--; }}; 

sub timer 
{ 
    my($n, $code, $tag) = @_; 
    my $t1 = gettimeofday; 
    $s = 1; 
    &$code(factorial_of); 
    my $t2 = gettimeofday; 
    my $md5 = md5_hex($s); 
    printf "Loop %d: %-33s %09.6f (%s)\n", $n, $tag, $t2 - $t1, $md5; 
} 

my $count = 1; 
timer($count++, $l1, 'for - Count up $s *= $i:'); 
timer($count++, $l2, 'for - Count up $s = $s * $i:'); 
timer($count++, $l3, 'for - Count down $s *= $i:'); 
timer($count++, $l4, 'for - Count down $s = $s * $i:'); 
timer($count++, $l5, 'while - Count up $s *= $i:'); 
timer($count++, $l6, 'while - Count up $s = $s * $i:'); 
timer($count++, $l7, 'while - Count down $s *= $i:'); 
timer($count++, $l8, 'while - Count down $s = $s * $i:'); 

Exemple de sortie (checksum MD5 comprimé pour éviter la rupture de la ligne - la pleine valeur est 584b3ab832577fd1390970043efc0ec8):

Loop 1: for - Count up $s *= $i:  12.853630 (584b3ab8...3efc0ec8) 
Loop 2: for - Count up $s = $s * $i: 20.854735 (584b3ab8...3efc0ec8) 
Loop 3: for - Count down $s *= $i:  14.798155 (584b3ab8...3efc0ec8) 
Loop 4: for - Count down $s = $s * $i: 23.699913 (584b3ab8...3efc0ec8) 
Loop 5: while - Count up $s *= $i:  12.972428 (584b3ab8...3efc0ec8) 
Loop 6: while - Count up $s = $s * $i: 21.192956 (584b3ab8...3efc0ec8) 
Loop 7: while - Count down $s *= $i:  14.555620 (584b3ab8...3efc0ec8) 
Loop 8: while - Count down $s = $s * $i: 23.790795 (584b3ab8...3efc0ec8) 

I. régulièrement voir une petite pénalité (< 1%) pour la boucle 'while' sur la boucle 'for' correspondante, mais je n'ai pas de bonne explication pour il.

+0

@Jonathan Leffler, merci beaucoup! Votre code d'illustration est très instructif pour moi. Merci :) – Mike

+0

@Joanthan, merci pour le code mis à jour.J'ai toujours pensé que $ s * = $ i 'et' $ s = $ s * $ i 'et $ i ++ et $ i-- faisaient la même chose de différentes manières Mais je me trompais.Merci beaucoup de l'avoir signalé :) J'ai maintenant changé de temps pour les scripts et maintenant j'ai: my $ now = time; mon $ n = shift; mon $ i = 2; mon $ s = 1; pour (; $ i <= $ n; $ i ++) { $ s * = $ i; } Et mon $ n = shift; mon $ i = 2; mon $ s = 1; While ($ i <= $ n) { $ s * = $ n; $ i ++; } Ils ressemblent. Résultat: alors que c'est plus rapide. Je ne suis pas sûr, mais y a-t-il quelque chose qui ne va pas dans la conception de mon expérience? J'ai exécuté votre code, le résultat était plus lent. – Mike

+1

@Mike: Je n'ai pas une bonne idée de ce que sont les problèmes résiduels. Les points principaux sont (1) le problème est principalement dans 'bigint' et (2) il est probable que les différences résiduelles 'while vs for' sont enfouies profondément dans le code octet Perl. J'ai eu quelques variations dans les timings - la plupart du temps de l'ordre de 0,1 secondes, sauf s'il y avait aussi une sauvegarde en cours (TimeMachine à une TimeCapsule); J'ai choisi 13000 comme numéro de test pour obtenir un nombre suffisant pour obtenir des temps raisonnables sans être trop grand pour ne pas être à l'aise avec les tests (1 heure est trop longue, par exemple). –

5

je serais tomber choqué s'il y avait effectivement une différence « réelle » entre tout et pour les boucles . En supposant qu'ils faisaient exactement la même chose, ils devraient être optimisés par l'interprète pour être plus ou moins identiques.

Je parierais que la différence n'était probablement rien de plus que d'autres processus qui se disputaient différemment pour les ressources au cours des deux exécutions.

Même s'il y avait une différence, ne vous laissez pas prendre par The Sad Tragedy of Micro-Optimization Theater.

+0

@Morinar, je viens de terminer l'article que vous avez suggéré. Je vois ce que vous dites, merci. – Mike

5

Une des clés de l'analyse comparative est de simplifier. Le problème posé est la vitesse de for par rapport à while. Mais l'expérience implique plusieurs complexités inutiles.

  • Les deux boucles ne sont pas aussi similaires qu'elles pourraient l'être. L'un utilise $s *= $n et l'autre utilise $s = $s * $i (comme le souligne Jonathan Leffler). L'un utilise $n-- et l'autre utilise $i++ (qui sait si elles diffèrent en vitesse?). Si nous nous intéressons à for par rapport à while, il n'est pas nécessaire d'impliquer bigint. Cela ne fait que confondre le sujet. En particulier, votre script while dépend d'un seul objet bigint ($s), alors que votre script for en utilise deux ($s et $i). Cela ne me surprend pas que le script for soit plus lent.

vos boucles Réécrire être aussi proche que possible, garder les factorielles assez petit pour que vous ne devez pas utiliser bigint, et utiliser le module Benchmark. Ensuite, vous pouvez courir une course en face-à-face de for vs while. Je serai curieux de voir ce que tu trouves.

+1

@FM, mon expérience était si mal conçue que mon inférence à partir du résultat était presque totalement sans rapport avec la question que je posais. C'était un échec total. Eh bien, merci de me laisser ces commentaires instructifs. On dirait que je peux toujours apprendre une chose ou deux de vous les gars :) – Mike

+1

@Mike Ne soyez pas trop dur avec vous-même. L'analyse comparative est délicate, et même les programmeurs expérimentés font des erreurs en les configurant. Par exemple: http://stackoverflow.com/questions/1083269/is-perls-unpack-ever-faster-than-substr et http://stackoverflow.com/questions/1960779/why-does-perls-tr-n -get-slow-and-slow-as-line-lengths-augmente. Votre indice de référence a peut-être été erroné, mais la question a été couronnée de succès parce que vous avez appris des choses utiles. :) – FMc

Questions connexes