2010-07-15 27 views
14

Estaba escribiendo un analizador de archivos en Perl, así que tuve que recorrer el archivo. El archivo consta de registros de longitud fija y quería crear una función separada que analiza el registro dado y llama a esa función en un bucle. Sin embargo, el resultado final resultó ser lento con archivos grandes y creo que no debería usar la función externa. Así que hice algunas pruebas simuladas con y sin llamada de función en un bucle:¿por qué las llamadas a funciones en bucles Perl son tan lentas?

[A]

foreach (1 .. 10000000) { 
$a = &get_string(); 
} 

sub get_string { 
return sprintf("%s\n", 'abc'); 
} 

[B]

foreach (1 .. 10000000) { 
$a = sprintf "%s\n", 'abc'; 
} 

medición mostró que un código se ejecuta alrededor de 3-4 veces más lento que el código B. Sabía de antemano que el código A se suponía que debía correr más lento, pero aún así me sorprendió que la diferencia fuera tan grande. También intenté ejecutar pruebas similares con Python y Java. En el código de Python, un equivalente era aproximadamente un 20% más lento que B y el código de Java corría más o menos a la misma velocidad (como se esperaba). Cambiar la función de sprintf a otra cosa no mostró ninguna diferencia significativa.

¿Hay alguna manera de ayudar a Perl a ejecutar esos bucles más rápido? ¿Estoy haciendo algo totalmente incorrecto aquí o es la característica de Perl que las llamadas a funciones son tales?

+0

lo que, precisamente, no get_string() hacer? – eruciform

+1

@roe Estamos suponiendo que es un stub y no usaste 'sprintf' solo para pegar una nueva línea en una cadena constante. Eso sería tonto. Entonces, ¿qué es lo que realmente hace? – Schwern

+0

raro, mi pantalla formateada extrañamente, no estaba allí antes. firefox goof .. – eruciform

Respuesta

8

El problema que estás planteando no tiene nada que ver con los bucles. Sus dos A y B ejemplos son los mismos en ese sentido. Por el contrario, el problema es la diferencia entre la codificación directa en línea y la invocación del mismo código a través de una función.

Las llamadas a funciones implican una sobrecarga inevitable. No puedo hablar de la cuestión de si y por qué esta sobrecarga es más costoso en Perl en relación con otros idiomas, pero puedo proporcionar una ilustración de una mejor manera de medir este tipo de cosas:

use strict; 
use warnings; 
use Benchmark qw(cmpthese); 

sub just_return { return } 
sub get_string { my $s = sprintf "%s\n", 'abc' } 

my %methods = (
    direct  => sub { my $s = sprintf "%s\n", 'abc' }, 
    function => sub { my $s = get_string()   }, 
    just_return => sub { my $s = just_return()   }, 
); 

cmpthese(-2, \%methods); 

Esto es lo que obtener Perl v5.10.0 (MSWin32-x86-multi-hilo). Muy aproximadamente, simplemente llamar a una función que no hace nada es tan costoso como ejecutar directamente nuestro código sprintf.

    Rate function just_return  direct 
function 1062833/s   --  -70%  -71% 
just_return 3566639/s  236%   --   -2% 
direct  3629492/s  241%   2%   -- 

En general, si usted necesita para optimizar algún código Perl para la velocidad y que está tratando de exprimir hasta la última gota de la eficiencia, la codificación directa es el camino a seguir - pero que a menudo viene con un precio de menos mantenibilidad y legibilidad. Sin embargo, antes de entrar en el negocio de tal micro-optimización, debe asegurarse de que su algoritmo subyacente sea sólido y de que tenga una idea clara de dónde residen realmente las partes lentas de su código. Es fácil perder mucho esfuerzo trabajando en lo incorrecto.

+0

Obtengo solo 1 o 2% de diferencia entre función y función con proto. Perl 5.10/Windows XP y Perl 5.8.5 i386/Linux 2.6.12 i386 y Perl 5.8.8 x86_64/Linux 2.6.18 x86_64. – Toto

+0

@ M42 Creo que quisiste comentar sobre la respuesta de dolmen. – FMc

+0

@FM: Sí, por supuesto. – Toto

12

Si su sub no tiene argumentos y es una constante como en el ejemplo, se puede obtener una mayor velocidad en marcha mediante el uso de an empty prototype "()" en la declaración substitución:

sub get_string() { 
    return sprintf(“%s\n”, ‘abc’); 
} 

Sin embargo, esto es probablemente un caso especial para su ejemplo que no coincide con su caso real. Esto es solo para mostrarte los peligros de los puntos de referencia.

Aprenderá este consejo y muchos más al leer perlsub.

Aquí es un punto de referencia:

use strict; 
use warnings; 
use Benchmark qw(cmpthese); 

sub just_return { return } 
sub get_string { sprintf "%s\n", 'abc' } 
sub get_string_with_proto() { sprintf "%s\n", 'abc' } 

my %methods = (
    direct  => sub { my $s = sprintf "%s\n", 'abc' }, 
    function => sub { my $s = get_string()   }, 
    just_return => sub { my $s = just_return()   }, 
    function_with_proto => sub { my $s = get_string_with_proto() }, 
); 

cmpthese(-2, \%methods); 

y su resultado:

      Rate function just_return direct function_with_proto 
function    1488987/s  --  -65%  -90%    -90% 
just_return   4285454/s  188%   --  -70%    -71% 
direct    14210565/s  854%  232%  --     -5% 
function_with_proto 15018312/s  909%  250%  6%     -- 
+3

La carpeta constante parece haberse vuelto más inteligente entre 5.10.0 y 5.10.1. Solía ​​ser Perl solo podía doblar constantemente expresiones muy simples. 5.10.1 ahora puede manejar cosas más complicadas como la llamada sprintf. – Schwern

+0

Mi punto de referencia estaba en StrawberryPerl 5.12.0.1. – dolmen

23

llamadas a funciones Perl son lentos. Apesta porque lo que realmente quieres hacer, descomponer tu código en funciones mantenibles, es lo que ralentizará tu programa. ¿Por qué son lentos? Perl hace muchas cosas cuando ingresa a una subrutina, debido a que es extremadamente dinámica (es decir, puede meterse con muchas cosas en el tiempo de ejecución). Tiene que obtener la referencia de código para ese nombre, comprobar que es un código ref, configurar un nuevo scratchpad léxico (para almacenar variables my), un nuevo alcance dinámico (para almacenar variables local), configurar @_ por nombrar algunos , compruebe en qué contexto fue llamado y transmita el valor de retorno. Se han realizado intentos para optimizar este proceso, pero no han pagado. Ver pp_entersub in pp_hot.c para los detalles sangrientos.

También hubo un error en las funciones de desaceleración 5.10.0. Si está utilizando 5.10.0, actualice.

Como resultado, evite llamar funciones una y otra vez en un bucle largo. Especialmente si está anidado. ¿Puedes guardar los resultados en caché, quizás usando Memoize? ¿El trabajo debe hacerse dentro del ciclo? ¿Tiene que hacerse dentro del ciclo interno? Por ejemplo:

for my $thing (@things) { 
    for my $person (@persons) { 
     print header($thing); 
     print message_for($person); 
    } 
} 

La llamada a header podría ser trasladado fuera de la @persons bucle de reducir el número de llamadas de @things * @persons sólo @things.

for my $thing (@things) { 
    my $header = header($thing); 

    for my $person (@persons) { 
     print $header; 
     print message_for($person); 
    } 
} 
1

El optimizador de perl es constante-doblando las llamadas sprintf en su código de muestra.

Puede deparse a ver que pasa:

$ perl -MO=Deparse sample.pl 
foreach $_ (1 .. 10000000) { 
    $a = &get_string(); 
} 
sub get_string { 
    return "abc\n"; 
} 
foreach $_ (1 .. 10000000) { 
    $a = "abc\n"; 
} 
- syntax OK 
Cuestiones relacionadas