Aus Linux-Magazin 03/2015

Mixed-Language-Programmierung in Perl

© Eugene Sergeev, 123RF

Der Perl-Interpreter perl ist in C geschrieben und bietet recht komfortable Schnittstellen, um ihn mit erstaunlich schnellen Zusatzfunktionen auf Maschinenebene aufzupeppen. Allerdings profitieren nur ganz bestimmte Anwendungen tatsächlich vom Mixed Language Programming.

Um gar nicht erst auf Abwege zu geraten, bietet es sich für den Einstieg in das Thema Mixed Language Programming an, gleich auf eine Variante im Perl-Umfeld zu sprechen zu kommen, die fehl am Platze ist. Ein Beispiel: Neulich war der Blog-Artikel eines österreichischen Security-Hobbyforschers [1] zu lesen, der zu Testzwecken einen kostenlosen Internetproxy mit Malware manipulierte. Das dort vorgestellte Perl-Skript enthielt Zeilen wie

system("/usr/bin/wget","-q","-O" [...]

und illustrierte einen häufig von Perl-Neulingen begangenen Denkfehler: Statt Perls ausgeklügelte Funktionen zu nutzen, mutieren ihre Perl-Programme zu mittels »system()« -Anweisungen zusammengeschusterten Shellskripten.

Im hier gezeigten Code-Snippet hätte der Autor besser das Modul LWP::Single vom CPAN genutzt und damit eine zuverlässigere Fehlerbehandlung und bessere Wartbarkeit erreicht. Manchmal ist auch die benutzte Shellfunktion selber bereits in Perl verfügbar: Ein »system(“chmod […]“)« ist deshalb unnötig und zudem umständlich, weil »chmod()« schon in Perl eingebaut ist.

Extend oder Embed?

Der Fachmann, der stattdessen die sinnvollen Anwendungen für die Verknüpfung von Perl mit einer anderen Programmiersprache, etwa mit C, betrachtet, unterscheidet grundsätzlich zwischen Extending und Embedding. Die Manualseite »perldoc perlembed« listet ganz am Anfang die einzelnen Varianten auf und verweist auf die jeweils zum Thema passende Dokumentation. Wer seine Perl-Skripte mit C-Code erweitern möchte, der betreibt Extending. Embedding ist hingegen das Einfügen eines Perl-Interpreters in ein schon vorhandenes C-Programm. Wer nicht gerade eine Perl-IDE in C schreibt, für den ist Embedding allerdings selten von Nutzen.

Mit C erweiterte Perl-Bibliotheken erfreuen sich hingegen großer Beliebtheit, und jeder Perl-Programmierer muss sich wohl ein paar Mal in seinem Berufsleben der oft nicht ganz einfachen Aufgabe stellen, eine solche Erweiterung selbst zu schreiben. Typische Erweiterungen in C implementieren neue Perl-Schnittstellen zu bereits vorhandenen C-Bibliotheken, entweder weil es in Perl noch kein Pendant zu Unix-Systemfunktionen wie beispielsweise »ptrace« gibt [2] oder weil eine rechenintensive Anwendung die Ausführung in einer maschinennahen Sprache nahelegt [3].

Vorsicht, Denkfehler!

Dabei wäre es ein gewaltiger Denkfehler anzunehmen, dass jedes Perl-Skript schneller liefe, wenn man es nur in C nachprogrammiert. Richtig ist zwar, dass der Perl-Interpreter seine Instruktionen generell langsamer abarbeitet als die CPU die kompilierten Anweisungen eines C-Programms durchjagt. Aber oft verbirgt sich hinter einer einzelnen Bytecode-Anweisung des Perl-Interpreters der Code aus Dutzenden von C-Programmzeilen, sodass der direkte Vergleich der Laufzeiten pro Zeile Programmcode hier in die Irre führt.

Außerdem verbringt ein typisches Programm seine Zeit hauptsächlich mit Warten. Warten auf Daten, die von der Festplatte oder einem Socket eintrudeln, oder einfach darauf, dass ein fremder Prozess oder Webserver auf eine gestellte Anfrage antwortet. Ist das der Fall, profitiert das entsprechende Perl-Skript praktisch niemals von einer Portierung auf schnellen C-Code.

Schneller bei Mathe

Verbringt ein Skript allerdings seine Zeit zu 100 Prozent damit, mathematische Formeln in For-Schleifen auszuwerten, ohne mal eine Pause einzulegen, um auf externe Daten zu warten, dann kann eine C-Erweiterung erstaunliche Performancegewinne bringen. Steigerungen um eine oder sogar mehrere Größenordnungen sind dann keine Seltenheit.

Vor der Neuerfindung des Rades empfiehlt es sich jedoch, in einschlägigen Repositories wie dem CPAN nachzusehen und herauszufinden, ob es sich tatsächlich lohnt, selbst in die Tasten zu greifen. Wer zum Beispiel Matrizenrechnungen oder allgemein rechenintensive mathematische Formeln durchackert, ist mit einem Modul wie PDL, das arithmetische Rechenoperationen in einer C-Erweiterung statt in Perls VM ausführt, normalerweise bestens bedient.

Listing 1 zeigt einen Benchmark, der zu allen 4000 mal 4000 Elementen einer Matrix einen konstanten Wert hinzuaddiert. Das erste Verfahren in der Funktion »madd()« nutzt reines Perl, um in zwei verschachtelten For-Schleifen über alle Elemente der Matrix zu iterieren und jeweils den Wert 1 zu addieren. Die Alternativmethode »madd_pdl()« ab Zeile 30 nutzt das CPAN-Modul PDL::Matrix, erzeugt eine neue Matrix in PDL und erhöht den Wert aller 16 Millionen Elemente mit einer einzigen Anweisung. Dass das simple »$mpdl += 1« dabei so viel tut, liegt daran, dass das PDL-Modul den Operator »+« überladen hat.

Listing 1

Matrix-add-Benchmark

01 #!/usr/local/bin/perl -w
02 use strict;
03 use PDL;
04 use PDL::Matrix;
05 use Benchmark;
06
07 my $dim = 4_000;
08
09 my $m1 = init();
10 my $m2 = init();
11
12 timethese( 10, {
13     "madd",     sub { madd( $m1 ) },
14     "madd_pdl", sub { madd_pdl( $m2 ) },
15 } );
16
17 ###########################################
18 sub madd {
19 ###########################################
20     my( $m ) = @_;
21
22     for my $x ( 0..$dim-1 ) {
23         for my $y ( 0..$dim-1 ) {
24             $m->[ $x ]->[ $y ] += 1;
25         }
26     }
27 }
28
29 ###########################################
30 sub madd_pdl {
31 ###########################################
32     my( $m ) = @_;
33
34     my $mpdl = PDL::Matrix->pdl( $m );
35
36     $mpdl += 1;
37 }
38
39 ###########################################
40 sub init {
41 ###########################################
42     my $m   = [];
43
44     for my $y (1..$dim) {
45         push @$m, [ ( 1 ) x $dim ];
46     }
47
48     return $m;
49 }

Benchmarkbeweise

Zum Geschwindigkeitsvergleich nutzt das Skript die Funktion »timethese()« in Zeile 12. Sie stammt aus dem Modul Benchmark vom CPAN, nimmt einen Zähler entgegen (der in diesem Fall den Wert 10 hat) und führt die ebenfalls der Funktion übergebenen Subroutinen genauso oft aus. Dabei misst es die verstrichene Zeit pro Durchgang und gibt anschließend aus, wie viel Zeit die alternativen Implementierungen zum Ablauf jeweils benötigten.

Abbildung 1 bestätigt die Vermutung, dass die Matrizenrechnung um ein Vielfaches schneller abläuft, im Beispiel etwa um den Faktor drei, wenn sie wie im PDL-Modul in C statt in Perl implementiert ist. Der reine Perl-Code verschwendet besonders bei großen Matrizen enorm viel Zeit.

Abbildung 1: Das in C geschriebene Mathe-Modul ist mehr als dreimal so schnell wie reines Perl.

Abbildung 1: Das in C geschriebene Mathe-Modul ist mehr als dreimal so schnell wie reines Perl.

Außerdem kann der C-Code eine Matrix wirklich nur als Array von n mal m Integer-Elementen ablegen, während der Perl-Code für die Ganzzahlen Skalare benutzt, deren Speicherbedarf in Reihen-Arrays bei der gleichen Anzahl von Matrixelementen um ein Vielfaches höher liegt. Zum Performancenachteil kommt also auch noch Platzverschwendung.

Aber auch Nachteile

Die Perl-Snapshot-Reihe hat das Thema von Perls C-Erweiterungen in ihren mehr als 17 Jahren Laufzeit schon einige Male aufgegriffen. Ob es ein aufwändiger Bilderkennungsalgorithmus war, der aus Performancegründen in maschinennahem Code laufen musste [3], oder ein einfaches Testskript, das den Perl-Interpreter bewusst zum Absturz brachte [4], oder ob es darum ging, in Perl den Zugang zu nur in C verfügbaren Systemschnittstellen wie der Ptrace-API zu öffnen [5], Praxisbeispiele gab es genug.

Bei allen sofort erkennbaren Vorteilen darf der Entwickler, der sich für eine C-Erweiterung entscheidet, aber nicht vergessen, auch die sich daraus ergebenden Nachteile abzuwägen. Ein Problem ist offensichtlich, dass ein kompiliertes C-Programm genau auf der Plattform läuft, auf der es kompiliert wurde. Aber Entwickler lieben Skriptsprachen gerade deshalb, weil die damit implementierten Softwareprodukte oft gänzlich ohne oder ohne großen Aufwand auf einer Vielzahl von Zielarchitekturen laufen.

Auch wenn ein Perl-Skript auf den Netzwerkstack zugreift, das Dateisystem manipuliert oder auf Datenbanken herumorgelt, ist das selbst bei komplett unterschiedlichen Architekturen wie Linux, OS X und Windows oft ohne Anpassung des Quellcodes möglich. Das ändert sich ganz schnell, sobald eine kompilierte Erweiterung hinzukommt: Dann muss das Zielsystem auf einmal einen Compiler bereitstellen, und kleine, aber kritische Unterschiede zwischen den Operationssystemen, die die Skriptsprache sonst elegant vertuscht, treten nun in aller Hässlichkeit zutage.

Ein weiterer, oft übersehener Nachteil von Modulen mit C-Erweiterungen ist deren Abhängigkeit von Perl-internen Strukturen, von denen der C-Code in Erweiterungen regen Gebrauch macht, wenn er mit dem Interpreter kommuniziert, um zum Beispiel Parameter entgegenzunehmen oder Ergebnisse zurückzusenden.

Ändern sich nun die Perl-Interna, was, wie Veteranen augenrollend zu berichten wissen, in der Vergangenheit leider immer wieder vorgekommen ist, reicht es bei einem routinemäßigen Upgrade von zum Beispiel Perl 5.18 nach Perl 5.20 nicht mehr, das Modul mitsamt seinem XS-Code neu zu kompilieren. Vielmehr erfordert das Perl-Upgrade nun die Anstellung eines Spezialisten, der hinab in den grusligen C-Code steigen muss, um ihn an die Erfordernisse der neuen Perl-Version anzupassen.

XS oder einfacher

Die traditionelle Methode, Perl und C-Erweiterungen miteinander zu verknüpfen, heißt XS und besteht aus einer Beschreibungssprache für das zu erzeugende Interface, das ein Buildtool dann in C-Code umwandelt. Das Ganze wird ganz schnell ganz kompliziert, und deshalb entstehen C-Erweiterungen heutzutage flotter mit dem Modul Inline vom CPAN, wie im Snapshot vor Jahren schon einmal beschrieben [5].

Von häufig genutzten Modulen auf dem CPAN kommen manchmal XS-Versionen heraus, zum Beispiel von dem Json-Parser Json, der als JSON::XS die Daten um ein Vielfaches schneller ein- und ausliest. Um den Unterschied zu messen, generiert Listing 2 eine ellenlange Json-Datei mit einem Hash, der eine Million Einträge aufweist. Wie Abbildung 2 zeigt, spart die XS-Version 90 Prozent der Zeit. Einzig und allein dadurch, dass der Anwender den Code

Listing 2

genjson

01 #!/usr/local/bin/perl -w
02 use strict;
03 use Sysadm::Install qw(:all);
04 use JSON qw( to_json );
05
06 my $data = {};
07
08 for my $idx ( 1 .. 1_000_000 ) {
09     $data->{ "X$idx" } = $idx;
10 }
11
12 print to_json( $data );
Abbildung 2: Gegenüber dem normalen Json-Modul spart die XS-Version JSON::XS 90 Prozent Zeit ein.

Abbildung 2: Gegenüber dem normalen Json-Modul spart die XS-Version JSON::XS 90 Prozent Zeit ein.

use JSON;

durch

use JSON::XS;

ersetzt, schrumpft die Laufzeit des Skripts (Listing 3), wie aus Abbildung 2 ersichtlich, von 10 Sekunden auf 1 Sekunde in Listing 4. Das Json-Modul vom CPAN ist übrigens so schlau, auch dann die XS-Version zu laden, wenn der User sie nicht explizit vorgibt, es aber feststellt, dass sie im Dateisystem verfügbar ist. Zur Vergleichsmessung musste daher die Datei »JSON/XS.pm« explizit aus der Perl-Installation getilgt werden.

Listing 4

jsonxsreader

1 #!/usr/local/bin/perl -w
2 use strict;
3 use Sysadm::Install qw(:all);
4 use JSON::XS qw( decode_json );
5
6 data = decode_json( slurp "test.json" );

Listing 3

jsonreader

1 #!/usr/local/bin/perl -w
2 use strict;
3 use Sysadm::Install qw(:all);
4 use JSON qw( decode_json );
5
6 decode_json( slurp "test.json" );

Durch Bilddaten rattern

Das elegante Verfahren, C-Code mit dem Inline-Modul vom CPAN einzubinden, zeigt die im Folgenden implementierte Funktion zur Ermittlung der mittleren Helligkeit eines digitalen Bildes. Hierzu iteriert die Funktion über alle im Bild enthaltenen Pixel, extrahiert zu jedem die gespeicherten Farbwerte für Rot, Grün und Blau und dividiert diese der Einfachheit halber einfach durch 3, summiert das Ergebnis auf und teilt es bei Programmschluss dann durch die Anzahl aller untersuchten Pixel.

Zum Vergleichen der Laufzeiten zwischen Perl- und C-Code implementiert Listing 5 das Verfahren zunächst in reinem Perl unter Zuhilfenahme des CPAN-Moduls Imager. Zeile 6 liest das digitale Foto, das unter »test.jpg« auf der Festplatte liegt, in den Imager-Speicher, Zeile 11 ermittelt daraus die Breite und Höhe des dekomprimierten Bildes.

Listing 5

imgavg

01 #!/usr/local/bin/perl -w
02 use strict;
03 use Imager;
04
05 my $i = Imager->new();
06 $i->read(
07     file => "test.jpg",
08     type => "jpeg" ) or die;
09
10 print imgavg( $i,
11    $i->getwidth(), $i->getheight() ), "\n";
12
13 ###########################################
14 sub imgavg {
15 ###########################################
16   my( $i, $width, $height ) = @_;
17
18   my $sum = 0;
19
20   for( my $x=0; $x<$width; $x++ ) {
21     for( my $y=0; $y<$height; $y++ ) {
22
23       my $color =
24         $i->getpixel( x => $x, y => $y );
25
26       my ($red, $green, $blue, $alpha) =
27         $color->rgba();
28
29       $sum +=
30         ( $red + $green + $blue ) / 3;
31     }
32   }
33
34   return int( $sum / $width / $height );
35 }

In der Funktion »imgavg()« ab Zeile 14 iterieren zwei For-Schleifen über alle Bildzeilen, und für jedes gefundene Pixel holt die Imager-Funktion »getpixel()« den zugehörigen Farbwert hervor. Dessen Rot-, Grün- und Blau-Komponenten extrahiert in Zeile 27 die Methode »rgba()« , sodass Zeile 29 deren Werte nur noch mitteln und zur Gesamtsumme »$sum« addieren muss. Am Ende der Funktion »imgavg()« wird die Summe durch die Bildbreite und -länge dividiert (also durch die Anzahl der Pixel) und auf Ganzzahlen gerundet. Abbildung 3 misst dafür 22,4 Sekunden Rechenzeit bei einer 8 Megapixel großen Bilddatei.

Abbildung 3: Das mit Inline aufgepeppte Skript zur Ermittlung der mittleren Bildhelligkeit läuft 100-mal so schnell wie das in reinem Perl.

Abbildung 3: Das mit Inline aufgepeppte Skript zur Ermittlung der mittleren Bildhelligkeit läuft 100-mal so schnell wie das in reinem Perl.

Hundertmal schneller

Listing 6 dagegen implementiert die teure For-Schleife in C und lässt das Perl-Skript nur die Ein- und Ausgabe erledigen. Es erlaubt sich sogar den Luxus, die Mitteilung der Farbkanäle eines einzigen Pixels in eine extra C-Funktion »brightness()« zu packen. Das C-API des Imager-Moduls, das wegen der Inline-Option »WITH => ‘Imager’« vorliegt, erhält die Farbkanäle eines bestimmten Pixels im Bild durch die Funktion »i_gpix()« , der Rest der C-Implementierung ist dem Perl-Code sehr ähnlich, nur dass der maschinennahe Zugriff viel Zeit spart.

Listing 6

imgavg-inline

01 #!/usr/local/bin/perl -w
02 use strict;
03 use Imager;
04
05 my $i = Imager->new();
06 $i->read(
07     file => "test.jpg",
08     type => "jpeg" ) or die;
09
10 print imgavg( $i,
11    $i->getwidth(), $i->getheight() ), "\n";
12
13 use Inline C =>
14   <<'EOT' => WITH => 'Imager';
15
16 int brightness(i_color *val) {
17     return((val->channel[0] +
18             val->channel[1] +
19             val->channel[2])/3);
20 }
21
22 int imgavg( Imager im, int width,
23             int height ) {
24       i_color val;
25       int     x,y;
26       float   sum;
27       long    pixels;
28
29       for( x=0; x<width; x++ ) {
30           for( y=0; y<height; y++ ) {
31               i_gpix(im, x, y, &val);
32               sum += brightness( &val );
33           }
34       }
35
36       return sum / width / height;
37 }
38
39 EOT
40
41 1;

Bei der gleichen 8 Megapixel großen Bilddatei zeigt Abbildung 3, dass die C-Implementierung in 2 Zehntelsekunden fertig ist, während das in reinem Perl geschriebene Skript volle 22 Sekunden damit herumtrödelt. Das ist ein Faktor von 100 und sehr beachtlich.

Wer mehr mit Perls Erweiterungsschnittstelle herumspielen will, findet auf der Manualseite »perlxstut« einige einfache Beispiele zur Einbindung von C-Code mit XS. Das wohl umfassendste Werk zum Thema Perl-Erweiterungen in C/C++ ist [6], auch der etwas in die Jahre gekommene Wälzer [7] präsentiert auf 30 Seiten ein gutes Fundament.

Infos

  1. Christian Haschek, “Why are free proxies free?”: https://blog.haschek.at/post/fd9bc
  2. Michael Schilli, “Prozess-Spion”: Linux-Magazin 04/08, S. 114, https://www.linux-magazin.de/Ausgaben/2008/04/Prozess-Spion
  3. Michael Schilli, “Zufall unter Beobachtung”: Linux-Magazin 05/07, S.98, https://www.linux-magazin.de/Ausgaben/2007/05/Zufall-unter-Beobachtung
  4. Michael Schilli, “Getriebeschaden”: Linux-Magazin 01/07, S. 120,https://www.linux-magazin.de/Ausgaben/2007/01/Getriebeschaden
  5. Michael Schilli, “Eingang zur Unterwelt”: Linux-Magazin 02/03, S. 95,http://perlmeister.com/snapshots/200302/index.html
  6. Tim Jenness, Simon Cozens, “Extending and Embedding Perl”: Manning, 2002
  7. Peter Wainwright, “Pro Perl”: Apress, 2005
DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 4 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Nach oben