Samstag, 2. August 2008

Perl als Portierungshilfe

Um Code von einer Programmiersprache in eine andere zu portieren, ist der Perl-Interpreter ein grossartiger Gehilfe: Da man sich ja in Programmiersprachen sowieso bemüht, für eine Maschine verständlich zu sein, sind die entstehenden Anweisungen und Ausdrücke für die Regex Engine von Perl leichte Kost. Grössere Codestrecken können - dank der enormen Flexibilität der regulären Ausdrücke in Perl - nach einem Schema automatisiert übertragen werden. Die Alternative, eine manuelle Portierung, ist nicht nur mühsamer; sie bringt auch immer das Risiko, dass sich Tippfehler einschleichen.

So ist für mein aktuelles Assemblerprojekt eine Fourier-Reihenentwicklung der Sonnenposition nach Simon Newcomb, die in Java vorliegt, nach Assembler zu portieren. Die Reihen sind lang, und die Gefahr, bei einem manuellen Abschreiben Tippfehler zu machen, ist demnach hoch. Für zwei Aufgaben kommt mir hierbei Perl zunutze:

  1. Ich will alle in der Java-Routine im Gradmass notierten Zahlen in Bogenmass wandeln. Alle Zwischenrechnungen sollen in Assembler nämlich in Bogenmass durchgeführt werden, und nur das Endergebnis wird schliesslich im Gradmass zurückgegeben.
  2. Ich will die Terme der Reihenentwicklungen nach Möglichkeit automatisiert in Assemblersequenzen wandeln.


Die Teile der Java-Methode newcomb(), die ich portieren möchte, hänge ich in meinem Portierungs-Script newcomb.pl als sogenannten __DATA__ Block hinten an den Perl-Code an und kann im Perl-Programm selbst auf diesen Block mit dem vordefinierten Dateihandle DATA zugreifen, als handelte es sich um eine Datei.

Mit dem folgenden regulären Ausdruck ersetze ich jede im Java-Code vorkommende Zahl durch ihr Äquivalent im Bogenmass:
# Konvertierungsfaktor Grad -> Bogenmass:
my $conv = 0.017453292519943;
my $line;
foreach $line (<DATA>) {
# Original-Codezeile als Kommentar ausgeben
print "; $line";
# Zahlen, die einen Dezimalpunkt enthalten, in Bogenmass konvertieren
$line =~ s/\d*\.\d*(e\d+)?/{$&*$conv}/ge;
# Konvertierte Zeile ausgeben
print "$line";
}
__DATA__
public static double[] newcomb (double jd)
{
...
u = 11.250889e0 + t1*(double).00224572621492128
...

Die obige Beispielzeile erzeugt etwa die Ausgabe
; u   =  11.250889e0 + t1*(double).00224572621492128
u = 0.196365056826409 + t1*(double)3.91953165487255e-005

Der reguläre Ausdruck hat also alle durch das Muster als Gleitkommazahlen erkennbare Zeichenfolgen der Reihe nach durch ihr Produkt mit $conv ersetzt. Hierzu braucht man neben dem Modifier g für wiederholte Mustersuche den nur in Perl verfügbaren Modifier e, der es erlaubt, in den Ersetzungsausdruck in geschweiften Klammern Perl-Code einzufügen. In diesem Fall wird das Produkt {$&*$conv} gebildet: Die komplette Fundstelle des regulären Ausdrucks ($&) wird - wegen des Vorkommens in einem Produkt - vom Perl-Interpreter als Gleitkommazahl geparsed und dann mit dem Konvertierungsfaktor $conv multipliziert. Das Ergebnis ist der auszugebende Ersetzungsausdruck.

Nun komme ich zur zweiten Anforderung. Der längste Teil der Java-Methode newcomb() besteht aus Fouriertermen mit den mittleren Anomalien der Planeten als Argumente, die alle ähnlich wie der folgende aussehen und alle zum Ergebnis aufaddiert werden müssen:
+ 0.00202*cos(2.5987952e0 + 2*g2 -   g)

Hier bietet es sich an, zunächst die mittleren Anomalien der Planeten (das sind die Variablen g2, g etc.) zu errechnen und deren Vielfache in lokalen Arrays abzulegen, die am Anfang des neuen Unterprogramms etwa wie local g2[6]:real8 deklariert werden. Dieses Vorgehen spart Zeit, da die Vielfachen der Anomalien so nur einmal errechnet werden müssen. Da es um niedrige ganzzahlige Vielfache handelt, werden sie am besten durch sukzessives Addieren errechnet.

Danach kann ein Ausdruck wie der obige in Assembler möglichst effizient etwa folgendermassen notiert werden:
  fld   FP8(2.5987952) ; Phase nach ST
fadd g2[1] ; 2*g2 addieren
fsub g[0] ; 1*g subtrahieren
fcos ; Cosinus
fmul FP8(0.00202) ; Mit Faktor multiplizieren
fadd dl2,st ; Ergebnis im Speicher aufaddieren (M+)
fstp st ; ... und vom Stapel entfernen (POP)


Das Vorgehen, um aus dem ersten (Java-)Ausdruck den zweiten zu gewinnen, ist:

  • Die variablen Teile aus der Programmzeile entnehmen
  • Falls die Programmzeile auf das Muster passt, die variablen Teile in ein Template einsetzen und dieses ausgeben.

Da es sich um ein mehrzeiliges Template handelt, bietet sich in Perl ein Hier-Dokument an. Der Portierungscode sieht dann ungefähr folgendermassen aus:
my $line;
my $conv = 0.017453292519943;
my ($a,$sincos,$p,$plmin,$n1,$ig,$plmin2,$n2);
my %addsub = ( '+'=>'fadd', '-'=>'fsub' );

foreach $line (<DATA>) {
print "; $line";
# Zahlen, die einen Dezimalpunkt enthalten, in Bogenmass konvertieren
$line =~ s/\d*\.\d*(e\d+)?/{$&*$conv}/ge;
# Regulärer Ausdruck für die Fourierterme:
undef $plmin2;
($a,$sincos,$p,$plmin,$n1,$ig,$plmin2,$n2) =
($line =~ m/
(\d+\.?\d*) # Faktor (a)
\s*\* # *
\s*(sin|cos) # sin oder cos
\s*\( # (
\s*(\d+\.?\d*)(?:e0)? # Phase
\s*([+-]) # + oder -
\s*(\d\s*)?\*? # ganze Zahl *
\s*(g\d) # g2, g3, ...
\s*([+-]) # + oder -
\s*(\d\s*)?\*? # ganze Zahl *
\s*g # g
/ix ) ;

if (defined $plmin2) {
$n1 = ($n1 and ($n1 > 0)) ? ($n1-1) : 0;
$n2 = ($n2 and ($n2 > 0)) ? ($n2-1) : 0;
print <<TEMPL;
fld FP8($p)
$addsub{$plmin} $ig\[$n1]
$addsub{$plmin2} g\[$n2]
fcos
fmul FP8($a)
fadd dl2,st
fstp st
TEMPL
}
else {
print $line;
}
}
__DATA__
public static double[] newcomb (double jd)
{
..

Dieses Perl-Programm erzeugt die gewünschten Assemblercodefragmente für die Reihenentwicklungen nach der mittleren Anomalie. Diese Teile können, so wie sie ausgegeben werden, in die zu schreibende Assemblerroutine übernommen werden. Für die übrigen Codeteile wurden alle Gleitkommazahlen von Gradmass in Bogenmass gewandelt, was die Portierung immerhin erleichtert.

Beachten Sie nebenbei, dass der schöne Mechanismus der Interpolation bei der Ausgabe des Hier-Dokuments TEMPL wirksam wird: Durch das blosse Aufführen der Variablen im Text werden diese zur Laufzeit durch ihren Inhalt ersetzt. Ein idealer Mechanismus übrigens auch für HTML-Templates.

Kommentare :

ReneeB hat gesagt…

Der Reguläre Ausdruck im ersten Beispiel sollte eher so aussehen:


$line =~ s/(\d*\.\d*(?:e-?\d+)?)/$1*$conv/ge;


Als erstes habe ich bei dem Exponential-Teil noch ein "-?" hinzugefügt, falls die Zahl einen negativen Exponenten hat, sollte der reguläre Ausdruck dennoch matchen, wobei das minus natürlich optional ist.


Dann habe ich aus der Gruppierung eine "non-capturing group" gemacht, weil ich dieses Ergebnis in keiner Variablen speichern möchte.


Als nächstes noch den kompletten Suchausdruck in Klammern gepackt, damit ich im Ersetzungsteil $1 statt $& verwenden kann. Dir Verwendung von $& bremst alle Regulären Ausdrücke in einem Programm aus.

Rüdiger Plantiko hat gesagt…

Hallo René,

danke für Deinen Verbesserungsvorschlag. Du hast natürlich in allem recht, jedoch hat auch mein regulärer Ausdruck bereits seine Dienste getan. Ich habe - aus purer Faulheit - genau den Ausdruck gewählt, der mir die tatsächlich in der Javamethode vorkommenden Gradzahlen alle findet. Dabei kamen aber keine mit negativen Exponenten vor.

Aus demselben Grund - Faulheit - habe ich mir den Luxus erlaubt, mit dem langsamen $& zu arbeiten, denn für eine Handvoll Gleitkommazahlen ist die dadurch erzeugte höhere Systemlast kein Thema.

Dennoch finde ich Deinen Hinweis natürlich wichtig, denn es gibt natürlich auch Einsatzgebiete von Perl, in der es auf Performance ankommt und wo man besser nicht $& verwenden sollte.

Gruss,
Rüdiger