2010-07-15 15 views
14

Stavo scrivendo un parser di file in Perl, quindi ho dovuto eseguire il loop del file. Il file è costituito da record a lunghezza fissa e volevo creare una funzione separata che analizzi il dato dato e chiami quella funzione in un ciclo. Tuttavia, il risultato finale è diventato lento con file di grandi dimensioni e la mia ipotesi era che non dovrei usare la funzione esterna. Così ho fatto alcune prove preparatorie con e senza chiamata di funzione in un ciclo:perché le chiamate di funzione in Perl vanno in loop così lentamente?

[A]

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

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

[B]

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

misurazione ha dimostrato che un codice viene eseguito circa 3-4 volte più lento del codice B. Sapevo in anticipo che il codice A doveva funzionare più lentamente, ma sono rimasto sorpreso che la differenza fosse così grande. Ho anche provato a eseguire test simili con Python e Java. Nel codice Python un equivalente era di circa il 20% più lento di B e il codice Java scorreva più o meno alla stessa velocità (come previsto). La modifica della funzione da Sprint a qualcos'altro non ha mostrato alcuna differenza significativa.

Esiste un modo per aiutare Perl a eseguire tali cicli più velocemente? Sto facendo qualcosa di completamente sbagliato qui o è la caratteristica di Perl che le chiamate di funzione sono così generali?

+0

quello che, appunto, non get_string() fare? – eruciform

+1

@roe Supponiamo che si tratti di uno stub e non si è utilizzato 'sprintf' solo per incollare una nuova riga su una stringa costante. Sarebbe sciocco. Quindi cosa fa veramente? – Schwern

+0

strano, il mio schermo formattato in modo strano, non era lì prima. firefox goof .. – eruciform

risposta

8

Il problema che si sta sollevando non ha nulla a che fare con i loop. Entrambi gli esempi A e B sono gli stessi a tale riguardo. Piuttosto, il problema è la differenza tra la codifica diretta, in linea e la chiamata dello stesso codice tramite una funzione.

Le chiamate di funzione comportano un sovraccarico inevitabile. Non posso parlare per la questione del se e perché questo overhead è più costoso in Perl rispetto ad altre lingue, ma posso fornire un esempio di un modo migliore per misurare questo genere di cose:

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

Ecco quello che ho vai su Perl v5.10.0 (MSWin32-x86-multi-thread). Molto approssimativamente, semplicemente chiamando una funzione che non fa nulla costa tanto quanto direttamente eseguendo il nostro codice sprintf.

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

In generale, se è necessario ottimizzare qualche codice Perl per la velocità e si sta cercando di spremere fino all'ultima goccia di efficienza, la codifica diretta è la strada da percorrere - ma che spesso ha un prezzo di minore manutenibilità e leggibilità. Prima di entrare nel business di tale micro-ottimizzazione, tuttavia, si desidera assicurarsi che l'algoritmo sottostante sia solido e che si abbia una solida comprensione su dove risiedono effettivamente le parti lente del codice. È facile perdere un sacco di energie lavorando sulla cosa sbagliata.

+0

Ricevo solo 1 o 2% di differenza tra funzione e funzione con proto. Perl 5.10/windows XP e Perl 5.8.5 i386/Linux 2.6.12 i386 e Perl 5.8.8 x86_64/Linux 2.6.18 x86_64. – Toto

+0

@ M42 Penso che volevi commentare la risposta di dolmen. – FMc

+0

@ FM: Sì, certo. – Toto

12

Se il sub non ha argomenti ed è una costante, come nel tuo esempio, è possibile ottenere una grande velocità-up utilizzando an empty prototype "()" nella dichiarazione sub:

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

Tuttavia questo è probabilmente un caso speciale per il tuo esempio che non corrisponde al tuo caso reale. Questo è solo per mostrarti i pericoli dei benchmark.

Imparerai questo suggerimento e molti altri leggendo perlsub.

Ecco un punto di riferimento:

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

e il suo risultato:

      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 cartella costante sembra essere diventata più intelligente tra 5.10.0 e 5.10.1. Un tempo Perl poteva solo piegare costantemente espressioni molto semplici. 5.10.1 ora può gestire cose più complicate come la chiamata sprintf. – Schwern

+0

Il mio benchmark era su StrawberryPerl 5.12.0.1. – dolmen

23

chiamate di funzione Perl sono lenti. Fa schifo perché la cosa che vuoi fare, decomporre il tuo codice in funzioni gestibili, è la cosa che rallenterà il tuo programma. Perché sono lenti? Perl fa un sacco di cose quando entra in una subroutine, il risultato è estremamente dinamico (ad esempio si può fare un sacco di cose in fase di esecuzione). Deve ottenere il codice di riferimento per quel nome, controllare che sia un codice ref, impostare un nuovo scratchpad lessicale (per memorizzare le variabili my), un nuovo ambito dinamico (per memorizzare le variabili local), impostare @_ per nominarne alcune , controlla il contesto in cui è stato chiamato e passa il valore di ritorno. Sono stati fatti tentativi per ottimizzare questo processo, ma non hanno pagato. Vedi pp_entersub in pp_hot.c per i dettagli sanguinosi.

Inoltre, si è verificato un errore nelle funzioni di rallentamento 5.10.0. Se stai usando 5.10.0, aggiorna.

Come risultato, evitare di chiamare le funzioni più e più volte in un ciclo lungo. Soprattutto se è nidificato. Puoi mettere in cache i risultati, magari usando Memoize? Il lavoro deve essere fatto all'interno del ciclo? Deve essere fatto all'interno del ciclo più interno? Ad esempio:

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

La chiamata header potrebbe essere spostato fuori dal ciclo @persons riducendo il numero di chiamate da @things * @persons a solo @things.

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

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

L'ottimizzatore perl è costante-piegare i sprintf chiamate nel codice di esempio.

È possibile Deparse per vedere che succede:

$ 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 
Problemi correlati