2010-03-20 18 views
6

He realizado un pequeño experimento como se mostrará a continuación y parece que un ciclo while es más rápido que un ciclo for en Perl. Pero dado que el experimento fue bastante crudo, y el tema podría ser mucho más complicado de lo que parece, me gustaría escuchar lo que tiene que decir al respecto. Gracias como siempre por cualquier comentario/sugerencia :)En Perl, ¿es un ciclo while generalmente más rápido que un ciclo for?

En los siguientes dos pequeños scripts, he intentado while y for bucles por separado para calcular el factorial de 100.000. El que tiene el ciclo while tardó 57 minutos y 17 segundos en terminar, mientras que el equivalente en bucle for tomó 1 hora, 7 minutos y 54 segundos.

Script que tiene el bucle 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 que tiene para el bucle:

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

Probablemente esté tratando de optimizar lo incorrecto. Me pregunto por qué crees que esta parte del lenguaje en particular es tan importante. – Ether

+0

Ejecútelos cada uno varias veces, y probablemente promedien aproximadamente el mismo – CaffGeek

+0

@Chad, en realidad ya he probado el código un par de veces. Tomaron diferentes períodos de tiempo para terminar el mismo trabajo. Creo que la explicación de @Jonathan Leffler con el código de la ilustración tiene mucho sentido. – Mike

Respuesta

8

Los bucles no son equivalentes, y usted está principalmente golpeando el paquete bigint y no tiene nada que ver con for vs for per se.

El ciclo while usa la notación '$s *= $i' pero el ciclo for usa '$s = $s * $i'. Es lo suficientemente simple para demostrar que estos no son idénticos. Además, un ciclo cuenta hacia arriba; el otro cuenta hacia abajo. Esto afecta cuán grandes son los números que se multiplicarán. Es un efecto de segundo orden, pero no completamente despreciable.

[Actualización: revisada para mostrar solo una versión del código, con intervalos de segundos. Hay espacio para pensar que la impresión debe excluirse de los cálculos de tiempo; eso hace las cosas más desordenadas, así que no me molesté. He solucionado el error en la versión anterior: el bucle 4 era el mismo que el bucle 3; ahora no lo es. También introduje el formato de salida (aunque el manejo por debajo del segundo podría mejorarse, un ejercicio para el lector), y hay mejor "informe de progreso".]

Los resultados de sincronización en un Mac Mini (Snow Leopard 10.6.2) fueron:

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 

El guión:

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); 

Y aquí es una versión mucho más compacta de el código anterior, extendido para probar bucles "while" así como bucles "for". También se ocupa de la mayoría de los problemas de sincronización. Lo único que no es ideal (para mí) es que utiliza un par de variables globales, y arrugué ligeramente el código en el código refs para que todo encaje en una línea sin activar una barra de desplazamiento (en mi pantalla, de todos modos) Claramente, con un poco más de trabajo, la prueba podría ser envuelta en una matriz, de modo que la prueba se haga de forma iterativa: un ciclo a través de la matriz que ejecuta la función de temporizador en la información en la matriz. Etc ...es un SMOP - Materia simple de programación. (Imprime el hash MD5 del factorial, en lugar del factorial en sí, porque es más fácil comparar los resultados, etc.) Señaló un par de errores ya que estaba refactorizando el código anterior. Sí, MD5 no es seguro. pero yo no lo estoy usando para la seguridad; sólo para detectar cambios involuntarios) checksum

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:'); 

Ejemplo de salida (MD5 comprimido para evitar los saltos de línea - el valor total es 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. consistentemente observa una pequeña penalización (< 1%) para el bucle 'while' sobre el correspondiente bucle 'for', pero no tengo una buena explicación para eso.

+0

@Jonathan Leffler, ¡muchas gracias! Tu código de ilustración es muy instructivo para mí. Gracias :) – Mike

+0

@Joanthan, gracias por el código actualizado. Siempre pensé que $ s * = $ i 'y' $ s = $ s * $ i 'y $ i ++ y $ i-- estaban haciendo lo mismo de diferentes maneras pero estaba equivocado.Muchas gracias por señalar esto :) Ahora he cambiado el tiempo vs para los scripts y ahora tengo: my $ now = time; my $ n = shift; mi $ i = 2; my $ s = 1; para (; $ i <= $ n; $ i ++) { $ s * = $ i; } Y my $ n = shift; mi $ i = 2; my $ s = 1; while ($ i <= $ n) { $ s * = $ n; $ i ++; } Se ven similares. Resultado: mientras es más rápido. No estoy seguro, pero ¿hay algún problema con el diseño de mi experimento? He ejecutado su código, el resultado fue mientras es más lento. – Mike

+1

@Mike: No tengo una buena idea de cuáles son los problemas residuales. Los puntos principales son (1) el problema está principalmente en 'bigint' y (2) es probable que las diferencias residuales 'while vs for' estén enterradas profundamente en el código de Byte Perl. Obtuve algunas variaciones en los tiempos, la mayoría del orden de 0.1 segundos más o menos, a menos que también haya una copia de seguridad en ejecución (TimeMachine a TimeCapsule); Elegí 13000 como número de prueba para obtener un número lo suficientemente grande como para obtener tiempos razonables sin ser tan grande como para hacer que resulte incómodo realizar pruebas (por ejemplo, una hora es demasiado larga). –

5

que se caiga una descarga si no era en realidad ninguna diferencia "real" entre el tiempo y los bucles . Suponiendo que estuvieran haciendo exactamente lo mismo, el intérprete debería optimizarlos para que sean más o menos idénticos.

Apostaría que la diferencia probablemente no era más que otros procesos que competían de manera diferente por los recursos durante las dos ejecuciones.

Incluso si hubo una diferencia, no se deje atrapar en The Sad Tragedy of Micro-Optimization Theater.

+0

@Morinar, acabo de terminar el artículo que sugirió. Veo el punto que estás haciendo, gracias. – Mike

5

Una clave para la evaluación comparativa es simplificar. El problema que se plantea es la velocidad de for frente a while. Pero el experimento implica varias complejidades innecesarias.

  • Los dos bucles no son tan similares como podrían ser. Uno usa $s *= $n y el otro usa $s = $s * $i (como señala Jonathan Leffler). Uno usa $n-- y el otro usa $i++ (¿quién sabe si difieren en velocidad?).

  • Si estamos interesados ​​en for vs while, no hay necesidad de involucrar bigint. Eso solo confunde el tema. En particular, su script while depende de un solo objeto bigint ($s), mientras que su script for usa dos de ellos ($s y $i). No me sorprende que el script for sea más lento.

reescribir sus bucles de ser lo más similar posible, mantener los factoriales lo suficientemente pequeño como para que no tenga que utilizar bigint, y utilizar el módulo Benchmark. A continuación, puede ejecutar una carrera justa de cabeza a cabeza de for frente a while. Tendré curiosidad por ver lo que encuentres.

+1

@FM, mi experimento estaba tan mal diseñado que mi inferencia del resultado fue casi totalmente irrelevante para la pregunta que publiqué. Esto fue un fracaso total. Bueno, de todos modos, gracias por dejarme estos comentarios instructivos. Parece que siempre puedo aprender una cosa o dos de ustedes :) – Mike

+1

@Mike No seas tan duro contigo mismo. El benchmarking es complicado, e incluso los programadores experimentados cometen errores al configurarlos. Por ejemplo: http://stackoverflow.com/questions/1083269/is-perls-unpack-ever-faster-than-substr y http://stackoverflow.com/questions/1960779/why-does-perls-tr-n lento-lento-y-lento-como-largo-línea-aumentar. Su punto de referencia puede haber sido defectuoso, pero la pregunta fue exitosa porque aprendió algunas cosas útiles. :) – FMc

Cuestiones relacionadas