Freitag, 23. Mai 2008

Aufruf von Assembler aus Java

Bevor ich mich damit befasse, wie man Assemblerprogramme testgetrieben entwickelt, sei ein Kapitel über die Einbindung von Assemblercode in Java eingeschoben - ein Vorhaben, bei dem sich dem eingefleischten Java-Apostel die Fussnägel aufrollen dürften. Mir nicht. Denn aus meiner Sicht ist Java eine plattformunabhängige, Bytecode interpretierende Sprache neben vielen anderen. Aber so wie ich Perl-Programme für diese konkrete Maschine schreibe, obwohl Perl eine plattformunabhängige Sprache ist (die übrigens, wenn ich mag, auch Bytecode produzieren und ausführen kann), so kann mich auch nichts davon abhalten, Javaprogramme für eine konkrete Maschine zu schreiben. Die Plattformunabhängigkeit der Programmiersprache impliziert nicht, dass sie nur für plattformunabhängige Projekte eingesetzt werden muss. Vielleicht liegt für eine bestimmte Plattform eine besonders leistungsfähige oder aus anderen Gründen gut passende Bibliothek vor, und genau die soll im zu entwickelnden Programm zum Einsatz kommen.

Weil Sun das auch weiss, gibt es das Java Native Interface (JNI).

In dieser Übung soll es um Folgendes gehen: Es soll ein Java-Array von sehr vielen zufällig generierten Double-Werten im Gradmass (also zwischen 0 und 360) an eine Assemblerfunktion übergeben werden. Diese erzeugt einen neuen Java-Array, berechnet für jeden übergebenen Wert den Sinus, fügt ihn in den neu erzeugten Array ein und übergibt ihn an den Aufrufer. Schliesslich wird die gleiche Aufgabe mit einer Java-Methode erledigt, und die Laufzeiten werden verglichen. Da es sich auch um einen Test der Floating Point Unit (FPU) handelt, wollen wir die zu verwendende Assemblerbibliothek "fpu1" nennen.

Hier zunächst der Code des Java-Testprogramms:

class TestAsm {

static {
System.loadLibrary("fpu1");
}

// Umrechnung Bogenmass in Grad (für Funktion jsin360)
private static final double CONV = 180d / Math.PI;

// Die Assemblerfunktion
public static native double[] sin360(double[] x);

// Hauptprogramm
public static void main(String[] arg) {
double[] y,z;
double[] x;
double t1,t2,t3;
int iDim = 1000000;
int i;

if (arg.length > 0)
iDim = Integer.parseInt(arg[0]);

x = new double[iDim];
for (i=0; i<iDim; i++)
x[i] = 360d * Math.random( );

t1 = System.currentTimeMillis();
y = sin360(x); // In Assembler implementiert
t2 = System.currentTimeMillis();
z = jsin360( x ); // Java-Funktion zum Vergleich
t3 = System.currentTimeMillis();

System.out.println("iDim : "+ iDim );
System.out.println("Native: "+ (t2-t1));
System.out.println("Java : "+ (t3-t2));
System.out.println("N/J : "+ ((t2-t1)/(t3-t2)));
}

private static double[] jsin360(double[] x) {
double[] y = new double[x.length];
for (int i=0; i < x.length; i++)
y[i] = Math.sin(x[i]*CONV);
return y;
}
}


Mein Rechner gibt bei iDim = 3 Millionen mit einem "Java Heap Error" auf. Für kleinere Werte erhalte ich folgende Ausgaben:

iDim : 1000000
Native: 219.0
Java : 266.0
N/J : 0.8233082706766918

iDim : 2000000
Native: 375.0
Java : 468.0
N/J : 0.8012820512820513

iDim : 2500000
Native: 515.0
Java : 657.0
N/J : 0.7838660578386606

Das zeigt deutlich, dass für für die Berechnung von Gleitkommazahlen mit Assembler ein Potential von 20 bis 25 Prozent Laufzeitverbesserungen möglich ist. Dafür lohnt es sich wohl eher nicht, die Plattformunabhängigkeit aufzugeben. Der Grund, dass die Laufzeiten hier so nahe beieinanderliegen, ist, dass auch der Math.sin() nah an der FPU implementiert ist. Das Absteigen durch die verschiedenen Schichten vom Bytecode bis zum Prozessor erzeugt für Funktionen wie sin, die direkt an den Coprozessor delegiert werden können, einen relativ geringen Overhead.

Nun noch zum zugrundeliegenden Assemblerprogramm. Der nachfolgende Quellcode dokumentiert sich im wesentlichen selbst. Es ist, wie das obige Java-Programm, Testcode. Ob er den Spielregeln für produktiven Code folgt, wird sich noch zeigen, wenn ich mir diese Spielregeln in den nächsten Blogs überlege.

Da das aufrufende Java-Programm TestAsm heisst, müssen die in der DLL veröffentlichten Funktionen das Präfix Java_TestAsm besitzen. Hier gibt es nur die eine Funktion Java_TestAsm_sin360. Wenn man bestehende DLLs verwenden will, die solchen Namenskonventionen natürlich nicht unterliegen und in der Regel auch nicht daraufhin programmiert sind, Java-Parameterlisten entgegenzunehmen, benötigt man normalerweise Wrapper-Code: Eine zusätzliche DLL, die zwischen den Java-Parametern und den von der extern aufgerufenen DLL erwarteten Datenformaten hin- und zurückwandelt. Ein solcher Wrapper ist hier nicht nötig, da wir die DLL fpu1 ja explizit für den Gebrauch in der Javaklasse TestAsm.java entworfen haben. Sie wird mit den folgenden Kommandos generiert (wobei die fpu1.def die eine öffentliche Funktion Java_TestAsm_sin360 deklariert):

ml /c /coff fpu1.asm
link /SUBSYSTEM:WINDOWS /DLL /DEF:fpu1.def fpu1.obj

Die Funktion Java_TestAsm_sin360 ist für die C-Parameterkonvention deklariert. Die ersten beiden Parameter für den Aufruf einer nativen Funktion sind vom JNI fest vorgegeben. Der erste ist ein Handle der Java-Laufzeitumgebung, der zweite zeigt auf das aufrufende Objekt. Danach erst folgen die Aktualparameter des Java-Aufrufs, hier also ein Zeiger auf den Java-Array mit Double-Werten.

Mit dem ersten Parameter, dem Handle der Runtime, hat es die folgende Bewandnis: Es ist ein indirekter Zeiger (ein Zeiger auf einen Zeiger) auf eine Funktionstabelle. In dieser sind sequentiell die Adressen der verschiedenen nativ aufrufbaren Funktionen der Java Runtime aufgelistet. Will man solche Funktionen aufrufen, wie z.B. getArrayLength zur Ermittlung der Länge eines Arrays, so muss man den Offset des betreffenden Funktionszeigers in dieser Tabelle wissen. Hierzu sucht man im Include jni.h, das bei jeder Java-Distribution im Ordner include mitkommt, die Struktur JNINativeInterface_. Jedes Member dieser Struktur deklariert eine Funktion. Durch Abzählen bekommt man die Offsets: Zum Beispiel erhält man den Offset 02AC für die Funktion getArrayLength().

;---------------------------------------------------------------------
; Diese Übung zeigt den Gebrauch der Java-Environment,
; um an einen double[]-Array heranzukommen, einen neuen Java double[]
; zu instanziieren und mit Werten zu füllen:
; Hierzu wird eine Vektoroperation sin360 deklariert:
; Sie nimmt einen Vektor von Gradzahlen entgegen, berechnet für jede
; dieser Zahlen den Sinus, und gibt das Ergebnis als Array zurück
;---------------------------------------------------------------------

.386
.model flat, stdcall
option casemap :none ; case sensitive

;---------------------------------------------------------------------
; Windows-Symbole
;---------------------------------------------------------------------
include \masm32\include\windows.inc

;---------------------------------------------------------------------
; Prototypen der Java-Runtime-Prozeduraufrufe (s.u.)
;---------------------------------------------------------------------
getArrayLength proto stdcall ipjArray:dword
getDoubleArrayElements proto stdcall ipjArray:dword
newDoubleArray proto stdcall iSize:dword
releaseDoubleArray proto stdcall ipArray:dword, ipjArray:dword


.data
conv qword 180 ; Wird beim Laden der DLL durch Pi/180 ersetzt
pjA dd ? ; Zeiger auf übergebenen Java Double Array
pA dd ? ; Zeiger auf übergebenen double-Array
pAL dd ? ; Länge des Arrays
pjB dd ? ; Handle für Java Double Array
pB dd ? ; Zeiger auf Ergebnis-Array
pEnv dd ? ; Zeiger Auf Environment-Insanz der JRE
pJFunTab dd ? ; Beginn der Funktionstabelle

.code
;---------------------------------------------------------------------
; Code zum Laden und Entladen der DLL
;---------------------------------------------------------------------
LibMain proc hInstDLL:dword, reason:dword, unused:dword

.if reason == DLL_PROCESS_ATTACH
;---------------------------------------------------------------
; Initialisierungen
;---------------------------------------------------------------
fldpi
fidiv word ptr conv ; geht so nur mit Little Endian!
fst conv ; Konstante PI/180 einmal berechnen
mov eax, TRUE
ret

.elseif reason == DLL_PROCESS_DETACH

.endif

ret

LibMain Endp


;---------------------------------------------------------------------
; Den sin von einem Array von Gradzahlen ausrechnen,
; einen Java Array für das Ergebnis erzeugen
; und das Ergebnis zurückschreiben
;---------------------------------------------------------------------
Java_TestAsm_sin360 proc C ipEnv:dword,
ipClass:dword,
ijA:dword

;---------------------------------------------------------------
; Schnitstellenparameter in globale Variablen übertragen
;---------------------------------------------------------------
mov eax,ijA
mov pjA, eax ; Java Double Array

mov eax, dword ptr ipEnv
mov pEnv, eax ; Instanz der JRE

mov eax, dword ptr [eax]
mov pJFunTab, eax ; Funktionstabelle

;---------------------------------------------------------------
; Länge des Arrays holen & in globale Variable ablegen
;---------------------------------------------------------------
invoke getArrayLength, ijA
mov pAL, eax

;---------------------------------------------------------------
; getDoubleArrayElements aufrufen
;---------------------------------------------------------------
invoke getDoubleArrayElements, pjA
mov pA, eax ; Ergebnis merken

;---------------------------------------------------------------
; newDoubleArray aufrufen
;---------------------------------------------------------------
invoke newDoubleArray, pAL
mov pjB, eax ; Ergebnis merken

;---------------------------------------------------------------
; getDoubleArrayElements aufrufen
;---------------------------------------------------------------
invoke getDoubleArrayElements, pjB
mov pB, eax ; Ergebnis merken


;---------------------------------------------------------------
; Rechenschleife:
;---------------------------------------------------------------
; Die Elemente des Quell-Arrays holen,
; den Sinus bilden und im Ziel-Array fortschreiben
;---------------------------------------------------------------
mov ecx,pAL
mov eax,pA
mov ebx,pB
L1:
fld qword ptr [eax]
fmul conv
fsin
fstp qword ptr [ebx]
add eax,8
add ebx,8
loop L1

;---------------------------------------------------------------
; releaseDoubleArrayElements aufrufen
;---------------------------------------------------------------
invoke releaseDoubleArray, pB, pjB


;---------------------------------------------------------------
; Rücksprung: Array zurückgeben
;---------------------------------------------------------------
mov eax, dword ptr pjB
ret


Java_TestAsm_sin360 endp


;---------------------------------------------------------------
; Länge eines Java Double Arrays zurückgeben
;---------------------------------------------------------------
getArrayLength proc stdcall private \
ipjArray:dword ; Pointer auf jArray

push dword ptr ipjArray
push dword ptr pEnv
mov eax, pJFunTab
call dword ptr [eax+2ach]
ret

getArrayLength endp

;---------------------------------------------------------------
; Handle auf den elementaren double[]-Array beschaffen,
; der dem Java double[] Array zugrundeliegt
;---------------------------------------------------------------
getDoubleArrayElements proc stdcall private \
ipjArray:dword ; Pointer auf jArray

pushd 0
push dword ptr ipjArray
push dword ptr pEnv
mov eax, pJFunTab
call dword ptr [eax+2f8h] ; Offset für getDoubleArrayElements
ret ; Ergebnis ist Pointer auf Double-Array in EAX

getDoubleArrayElements endp

;---------------------------------------------------------------
; Neuen Java double[] Array erzeugen
;---------------------------------------------------------------
newDoubleArray proc stdcall private \
iSize:dword

push iSize ;
push dword ptr pEnv
mov eax, pJFunTab;
call dword ptr [eax+2d8h] ; Offset für newDoubleArray
ret ; Ergebnis ist Pointer auf jArray in EAX

newDoubleArray endp

;---------------------------------------------------------------
; Double Array in Java Array zurückschreiben (dies geht lt. Doku
; intern ohne Kopieren, muss aber explizit aufgerufen werden)
;---------------------------------------------------------------
releaseDoubleArray proc stdcall private \
ipArray:dword, ; Pointer auf normalen double[] Array
ipjArray:dword ; Pointer auf Java double[] Array

pushd 0 ; = allokierten Speicher für den Hilfsarray freigeben
push dword ptr ipArray
push dword ptr ipjArray
push dword ptr pEnv
mov eax, pJFunTab
call dword ptr [eax+318h] ; Offset für releaseDoubleArrayElements
ret

releaseDoubleArray endp

End LibMain

Keine Kommentare :