2010-07-15 10 views
14

Ich schrieb einen Dateiparser in Perl, musste also Datei durchlaufen. Die Datei besteht aus Datensätzen fester Länge, und ich wollte eine separate Funktion erstellen, die den gegebenen Datensatz analysiert und diese Funktion in einer Schleife aufruft. Das Endergebnis wurde jedoch bei großen Dateien langsam und ich vermutete, dass ich keine externen Funktionen verwenden sollte. So habe ich einige Dummy-Tests mit und ohne Funktionsaufruf in einer Schleife:Warum sind Funktionsaufrufe in Perl-Schleifen so langsam?

[A]

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

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

[B]

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

Measuring zeigte, dass ein Code läuft etwa 3-4mal langsamer als Code B. Ich wusste vorher, dass Code A langsamer laufen sollte, aber ich war überrascht, dass der Unterschied so groß ist. Außerdem wurde versucht, ähnliche Tests mit Python und Java durchzuführen. In Python-Code war ein Äquivalent etwa 20% langsamer als B und Java-Code lief mehr oder weniger mit der gleichen Geschwindigkeit (wie erwartet). Das Ändern der Funktion von sprintf zu etwas anderem zeigte keinen signifikanten Unterschied.

Gibt es eine Möglichkeit, Perl zu helfen, solche Schleifen schneller auszuführen? Mache ich hier etwas total falsches oder ist es Perls Feature, dass Funktionsaufrufe solch ein Overhead sind?

+0

, was genau zu tun ist get_string()? – eruciform

+1

@roe Wir gehen davon aus, dass es sich um einen Stub handelt, und du hast 'sprintf' nicht benutzt, um eine neue Zeile auf eine konstante Zeichenkette zu setzen. Das wäre albern. Was macht es wirklich? – Schwern

+0

seltsam, mein Bildschirm formatiert seltsam, es war nicht vorher da. firefox goof .. – eruciform

Antwort

8

Das Problem, das Sie ansprechen, hat nichts mit Schleifen zu tun. Ihre Beispiele A und B sind in dieser Hinsicht die gleichen. Das Problem ist vielmehr der Unterschied zwischen direkter Inline-Codierung und dem Aufruf desselben Codes über eine Funktion.

Funktionsaufrufe erfordern einen unvermeidlichen Overhead. Ich kann nicht auf die Frage der sprechen, ob und warum dieser Aufwand ist teurer in Perl im Vergleich zu anderen Sprachen, aber ich kann eine Darstellung einer besseren Art und Weise bietet diese Art der Sache zu messen:

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

Hier ist, was ich Holen Sie sich Perl v5.10.0 (MSWin32-x86-Multithread). Ganz grob gesagt, ist das Aufrufen einer Funktion, die nichts unternimmt, ungefähr so ​​kostspielig wie die direkte Ausführung unseres Codes sprintf.

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

Im Allgemeinen, wenn Sie einige Perl-Code für die Geschwindigkeit optimieren müssen und Sie versuchen, jeden letzten Tropfen Effizienz herauszupressen, direkte Codierung ist der Weg zu gehen - aber das kommt oft mit einem Preis weniger Wartbarkeit und Lesbarkeit. Bevor Sie sich mit solchen Mikrooptimierungen beschäftigen, möchten Sie jedoch sicherstellen, dass Ihr zugrunde liegender Algorithmus solide ist und Sie genau wissen, wo sich die langsamen Teile Ihres Codes tatsächlich befinden. Es ist leicht, eine Menge Mühe damit zu verschwenden, an der falschen Sache zu arbeiten.

+0

Ich bekomme nur 1 oder 2% Unterschied zwischen Funktion und Funktion mit Proto. Perl 5.10/Windows XP und Perl 5.8.5 i386/Linux 2.6.12 i386 und Perl 5.8.8 x86_64/Linux 2.6.18 x86_64. – Toto

+0

@ M42 Ich denke, du wolltest Dolmens Antwort kommentieren. – FMc

+0

@FM: Ja, natürlich. – Toto

12

Wenn Ihr Unter keine Argumente hat und ist eine Konstante, wie in Ihrem Beispiel können Sie eine große Geschwindigkeit-up erhalten, indem an empty prototype "()" in der Unter Erklärung mit:

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

Allerdings ist dies wahrscheinlich ein Sonderfall für Ihr Beispiel, das nicht zu Ihrem wahren Fall passt. Dies dient nur dazu, Ihnen die Gefahren von Benchmarks aufzuzeigen.

Sie lernen diesen Tipp und viele andere durch Lesen perlsub.Hier

ist ein Benchmark:

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

und sein Ergebnis:

      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

Der konstante Ordner scheint zwischen 5.10.0 und 5.10.1 klüger geworden zu sein. Es war mal Perl konnte nur sehr einfache Ausdrücke falten. 5.10.1 kann jetzt kompliziertere Dinge wie den sprintf-Aufruf behandeln. – Schwern

+0

Mein Maßstab war auf StrawberryPerl 5.12.0.1. – dolmen

23

Perl-Funktionsaufrufe sind langsam. Es ist schlecht, weil genau das, was du machen willst, deinen Code in wartbare Funktionen zerlegen, genau das ist, was dein Programm verlangsamt. Warum sind sie langsam? Perl macht eine Menge Dinge, wenn es in eine Subroutine eintritt, was darauf zurückzuführen ist, dass es extrem dynamisch ist (dh Sie können sich zur Laufzeit mit einer Menge Dinge anlegen). Er muss die Code-Referenz für diesen Namen abrufen, überprüfen, dass es sich um einen Code-Verweis handelt, einen neuen lexikalischen Notizblock einrichten (my Variablen speichern), einen neuen dynamischen Bereich (um local Variablen zu speichern), @_ einrichten, um nur einige zu nennen Überprüfen Sie, in welchem ​​Kontext es aufgerufen wurde und übergeben Sie den Rückgabewert. Es wurden Versuche unternommen, diesen Prozess zu optimieren, sie haben sich jedoch nicht ausgezahlt. Siehe pp_entersub in pp_hot.c für die blutigen Details.

Auch gab es einen Bug in 5.10.0 Verlangsamungsfunktionen. Wenn Sie 5.10.0 verwenden, aktualisieren Sie.

Vermeiden Sie es daher, Funktionen in einer langen Schleife immer wieder aufzurufen. Vor allem, wenn es verschachtelt ist. Können Sie die Ergebnisse zwischenspeichern, vielleicht mit Memoize? Muss die Arbeit innerhalb der Schleife gemacht werden? Muss es innerhalb der innersten Schleife getan werden? Zum Beispiel:

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

Der Aufruf von header könnte aus dem @persons Schleife Verringerung der Zahl der Anrufe von @things * @persons zog nach nur @things.

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

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

Der Perl-Optimierer faltet die sprintf Aufrufe in Ihrem Beispielcode.

Sie können es Deparse es zu sehen passiert:

$ 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