Leichtathletik

Wer eine neue, inkompatible Binärschnittstelle einführt, sollte dafür stichhaltige Gründe haben. Das mit Linux 3.4 eingeführte neue Application Binary Interface (ABI) x32 soll vor allem die Anwendungs-Performance auf 64-Bit-Rechnern verbessern.

vorlesen Druckansicht 5 Kommentare lesen
Lesezeit: 14 Min.
Von
  • Michael Riepe
Inhaltsverzeichnis

Seit nicht nur Server, sondern auch Desktop-PCs und Laptops über mehr als 4 GByte RAM verfügen, steigt die Verbreitung von 64-Bit-Betriebssystemen. Zwar können die Abkömmlinge von Intels Pentium Pro und AMDs Athlon mithilfe der Physical Address Extension (PAE) im 32-Bit-Modus mehr als 4 GByte adressieren, doch der Preis dafür ist eine kompliziertere Speicherverwaltung mit erhöhtem Rechenaufwand. Der mit AMDs Opteron eingeführte 64-Bit-Modus kommt ohne solche Tricks aus – und erlaubt weiterhin das Ausführen von Anwendungen im 32-Bit-Modus.

Unter Linux/x86_64 kommen die obendrein in den Genuss eines vergrößerten Adressraums. Ein 32-Bit-Kernel belegt üblicherweise die Adressen ab 0xc0000000, sodass der Anwendung nur 3 GByte zur Verfügung stehen. Ein x86_64-Kernel hingegen beansprucht nur wenige KByte der unteren 4 GByte für sich; 32-Bit-Prozesse können daher fast den gesamten Adressraum nutzen.

Anwendungen, die mehr RAM benötigen, müssen den 64-Bit-Modus nutzen. Der hat allerdings den Nachteil, dass Zeiger den doppelten Speicherplatz benötigen. Das trifft vor allem Programme, die in objektorientierten Sprachen wie C++ geschrieben sind oder viel mit verketteten Listen und Bäumen hantieren. Zwar kann man unter Umständen Platz sparen, indem man in alter Fortran-Manier Daten in Feldern ablegt und statt 64-Bit-Zeigern einen 32 Bit langen Index verwendet. Das Verfahren eignet sich jedoch nicht gut für Programme, die Speicher dynamisch per malloc oder new allozieren – heutzutage die Mehrheit.

Allerdings bietet der x86_64-Modus auch Vorteile. So stehen etwa Anwendungen nicht mehr sechs oder sieben, sondern bis zu 15 General-Purpose-Register zur freien Verfügung. Gleitkomma-Operationen können 16 SSE-Register und den SSE2-Befehlssatz (Streaming SIMD Extensions) nutzen, während im 32-Bit-Modus aus Kompatiblitätsgründen immer noch die klassische – sprich: hoffnungslos veraltete – i387-FPU mit einem acht Register tiefen Stack zum Einsatz kommt. Die Fülle an Registern erlaubt nicht nur schnelleren Zugriff auf häufiger genutzte Operanden: 64-Bit-Programme übergeben Parameter für Subroutinen in Registern, statt wie früher üblich auf dem Stack. Das spart zeitraubende Speicherzugriffe ein.

Shared Libraries profitieren ebenfalls vom 64-Bit-Modus. Sie verwenden in der Regel sogenannten Position-Independent Code (PIC), da beim Übersetzen nicht bekannt ist, unter welcher Adresse die Bibliothek später zu finden ist. 32-Bit-Code muss eine Reihe von Adressen zur Laufzeit berechnen und dafür obendrein ein weiteres Register opfern. 64-Bit-Code kann stattdessen die Adressierungsart „PC-relative“ der CPU nutzen.

Alles in allem wäre der 64-Bit-Modus die bessere Wahl, gäbe es dort nicht die für viele Anwendungen unnötig langen Zeiger. Informatik-Papst Donald E. Knuth kommentiert den Sachverhalt mit deutlichen Worten: „Es ist absolut idiotisch, 64-Bit-Zeiger zu haben, wenn ich ein Programm übersetze, das weniger als 4 Gigabyte RAM nutzt. Wenn solche Zeiger in einer struct vorkommen, verschwenden sie nicht nur den halben Speicher, sie werfen praktisch den halben Cache weg“ [a]. Ganz so extrem fällt der Speicher-Mehrverbrauch in der Praxis zwar nicht aus – nicht alle Variablen sind Zeiger. Ein Overhead von 20 bis 50 % ist jedoch durchaus realistisch.

Seit Version 3.4 unterstützt der Linux-Kernel eine dritte Variante, an der Knuth Gefallen finden dürfte: Das x32 genannte ABI bietet die Vorteile von 64-Bit-Code ohne den Nachteil langer Zeiger. Programme können den kompletten Registersatz nutzen; Datentypen wie int64_t, long long int und ihre vorzeichenlosen Gegenstücke verwenden obendrein die volle Breite der Integer-Register. Ein long int belegt wie im 32-Bit-Modus nur 4 Byte, Zeiger auf Daten und Funktionen ebenso. Vorhandener C/C++-Code sollte sich ohne Schwierigkeiten für x32 übersetzen lassen, sofern er nicht die Präprozessor-Variable __x86_64__ abfragt, deren Bedeutung sich geändert hat: Nur wenn zusätzlich __LP64__ definiert ist, erzeugt der Compiler 64-Bit-Code. Im x32-Modus ist stattdessen __ILP32__ gesetzt.

Quasi nebenbei haben die Kernel-Entwickler ein paar Altlasten beseitigt. Veraltete Systemaufrufe wie signal und sysctl gibt es bei x32 nicht mehr, Prozesse verwenden statt des langsamen Software-Interrupts int 0x80 nun grundsätzlich den schnelleren sysenter-Mechanismus. Systemaufrufe nutzen dieselben Schnittstellen wie im 64-Bit-Modus; ein im x32-Modus gesetztes Bit gestattet es dem Kernel, sie auseinanderzuhalten. Auf ausdrückliches Verlangen von Linus Torvalds ist der Datentyp time_t auf 64 Bit gewachsen, was den für 2038 erwarteten Weltuntergang durch Umkippen des Vorzeichenbits gut 290 Milliarden Jahre in die Zukunft verschiebt.

Noch ist x32 als experimentell eingestuft; wer beim Konfigurieren des Kernels die Variable CONFIG_EXPERIMENTAL nicht setzt, bekommt CONFIG_X86_X32 gar nicht zu sehen. Zudem benötigt man eine aktuelle Entwicklungsumgebung, die x32 unterstützt: binutils ab Version 2.22, glibc 2.16 oder neuer und mindestens gcc 4.7. Letzterer ist mit der Option - -with-multilib-list=’m32,m64,mx32’ zu konfigurieren; andernfalls unterstützt der fertige Compiler nur das alte 32-Bit-ABI. Wer einen Ada-Compiler benötigt, sollte gleich den neuen gcc 4.8 nehmen: In Version 4.7.2 lässt sich die Laufzeitbibliothek nicht für x32 übersetzen. Der GNU-Debugger gdb unterstützt x32 ab Version 7.5.

Der Einstieg ins neue 32-Bit-Geschäft ist wegen der gegenseitigen Abhängigkeiten etwas holprig: Zum Übersetzen des gcc braucht man die glibc, die sich wiederum ohne den Compiler nicht für x32 übersetzen lässt. Der Autor hat die Hürde mit einem Cross-Compiler genommen; wer sich die nicht unkomplizierte Bootstrap-Prozedur [b] ersparen will, kann x32-Binaries der aktuellen glibc 2.17 vom iX-FTP-Server herunterladen [c]. Das Paket enthält außer /usr/include/gnu/stubs-x32.h keine Header-Dateien, da die mit denen der x86_64-Version identisch sind. Da die Bibliotheken in den bislang nicht genutzten Verzeichnissen /libx32 und /usr/libx32 liegen, man kann sie parallel zu den vorhandenen installieren. Danach sind nur noch die binutils und gcc neu zu übersetzen, in dieser Reihenfolge. Wer den systemeigenen Compiler nicht überschreiben will, sollte den neuen in einem separaten Verzeichnis wie /opt/x32 installieren.

Im iX-Labor musste das x32-ABI eine Reihe von Benchmarks über sich ergehen lassen. Als Testmaschine kam ein PC mit Intels Core i7-860 (4 x 2,8 GHz, 8 GByte RAM) zum Einsatz, bei dem Hyperthreading, Turbo Boost und der Stromspar-Modus deaktiviert waren, um die Ergebnisse nicht zu verfälschen. Die Entwicklungsumgebung war mit gcc 4.7.2, binutils 2.23.1 und glibc 2.17 auf dem zu der Zeit aktuellen Stand, der Linux-Kernel (Version 3.7) nicht ganz.

Den Einfluss langer Zeiger auf die Performance demonstriert ein Mini-Benchmark, der Pseudo-Zufallszahlen in einen AVL-Baum einfügt. Die Knoten des Baums bestehen aus zwei Zeigern, dem Schlüssel – im Benchmark vom Typ uint32_t – und einem int-Feld für die Baumbalance. Im 64-Bit-Modus belegen sie 50 % mehr Speicher: 24 statt 16 Byte. Das schlägt sich in den Benchmark-Ergebnissen deutlich nieder. Übersetzt man das Programm mit –m64, benötigt es für 10 Millionen Einträge 9,95 Sekunden; mit –mx32 hingegen nur 9,37. Der klassische 32-Bit-Modus (–m32) ist mit 9,86 Sekunden langsamer als x32; daran dürfte vor allem die geringere Zahl verfügbarer Register schuld sein. Noch stärker macht die sich im altehrwürdigen Dhrystone-Benchmark bemerkbar. Dort schneidet x32 mit 25,3 MDhrystones/s am besten ab, dicht gefolgt vom 64-Bit-Modus (25,1 MDhrystones/s). Die 32-Bit-Version erreicht nur 19,3 MDhrystones/s.

Ein Sortier-Benchmark mit der Bibliotheksfunktion qsort demonstriert die Kosten von Funktionsaufrufen. qsort nutzt zum Vergleichen eine Funktion, deren Adresse der Aufrufer als Zeiger übergeben muss. Hier liegt der 64-Bit-Modus eine Nasenlänge vor x32: Er sortiert 10 Millionen 32-Bit-Zahlen in 1,92 Sekunden, x32 in 1,94. Im 32-Bit-Modus bleibt die Stoppuhr erst bei 2,89 s stehen. Noch größer wird der Rückstand beim Sortieren von 64-Bit-Zahlen. Während das Programm mit –m64 und –mx32 schon nach 2,01 beziehungsweise 2,02 s fertig ist, benötigt es im 32-Bit-Modus satte 3,74 Sekunden, weil für den Vergleich der Datenfelder mehrere 32-Bit-Operationen notwendig sind.

Gleitkomma-Rechnungen lassen sich im 32-Bit-Modus auf zwei Arten durchführen. Standardmäßig verwendet der Compiler die FPU, weil die in fast allen Prozessoren vorhanden ist – Ausnahmen sind der von Linux seit Version 3.8 nicht mehr unterstützte 386er und der 486SX, für den der Kernel nach wie vor eine Software-Emulation bietet. Mit der Option –mfpmath=sse kann man den Compiler anweisen, Code für die SSE-Einheit zu erzeugen. Allerdings nur innerhalb einer Funktion: Um die Kompatibilität zu wahren, legt der Compiler Funktionsargumente in beiden Varianten auf den Stack. Da der Aufrufer das Ergebnis in einem FPU-Register erwartet, müssen Funktionen, die die SSE-Einheit nutzen, es erst auf den Stack kopieren und danach in die FPU laden. x32- und 64-Bit-Code hingegen nehmen Argumente in Registern entgegen und geben das Resultat der Berechnung in xmm0 zurück. Tabelle 1 demonstriert den mit Funktionsaufrufen verbundenen Overhead am Beispiel einer einfachen Funktion, die ihre beiden double-Argumente addiert. Beim Erzeugen der drei Code-Schnipsel und Übersetzen aller Benchmarks kamen die Optimierungsoptionen –O3, –fno-unroll-loops und –fomit-frame-pointer zum Einsatz.


Ăśbergabe von FP-Argumenten und -Ergebnissen

Zum Evaluieren der Gleitkomma-Performance hat der Tester zwei Benchmarks ausgewählt, bei denen Funktionsaufrufe nur eine untergeordnete Rolle spielen: eine zweidimensionale komplexe FFT (Fast Fourier Transform, [d]) mit 1024 x 1024 Punkten, die zum Beispiel in der Bildbearbeitung zum Einsatz kommt, und die Berechnung der Mandelbrot-Menge (des „Apfelmännchens“, [e]) mit 4096 x 3072 Pixeln. Letztere basiert auf der einfachen Iterationsformel z = z * z + c, wobei z und c komplexe Zahlen sind. Die Benchmark-Programme führen die komplexen Berechnung allerdings komponentenweise mit reellen Zahlen (double oder float) aus, statt die C-Typen complex double und complex float zu verwenden. Der Grund dafür ist, dass gcc die Multiplikation zweier komplexer Zahlen grundsätzlich in einen Unterprogrammaufruf übersetzt, was die Performance stark beeinträchtigt. Vielleicht können sich die gcc-Entwickler ja irgendwann dazu durchringen, die Berechnung selbst – die lediglich vier reelle Multiplikationen, eine Addition und eine Subtraktion umfasst – inline auszuführen und nur für die Ausnahmebehandlung ein Unterprogramm aufzurufen.

Im Apfelmännchen-Benchmark zeigt sich die FPU der SSE-Einheit unterlegen: Sie schafft nur 175 Millionen Iterationsschritte in der Sekunde, während die SSE-Einheit bei double-Arithmetik 187 Millionen Schritte bewältigt, mit dem Datentyp float sogar 200 Millionen. Die FPU ist in beiden Disziplinen gleich schnell, weil sie intern mit einem 80-Bit-Datentyp („extended“) arbeitet. Das hat übrigens auch zur Folge, dass sich die Rechenergebnisse leicht unterscheiden.

Für die Fourier-Transformation mit double-Arithmetik benötigt die i387-FPU 1,10 Sekunden, die SSE-Einheit hingegen ist in 0,62 (64 Bit) bis 0,63 Sekunden (x32) fertig. Im 32-Bit-Modus mit –mfpmath=sse dauert die Berechnung 1,09 s, weil der Algorithmus die recht langsame Bibliotheksfunktion sincos aufruft – und die nutzt nicht die SSE-Einheit, sondern die FPU.

Mit float als Datentyp steht das Ergebnis der FFT wesentlich früher bereit. Das liegt zum einen daran, dass der Algorithmus nur die halbe Datenmenge lesen und schreiben muss, zum anderen daran, dass die float-Funktion sincosf schneller arbeitet als ihr double-Gegenstück sincos. In dieser Disziplin hat die FPU ausnahmsweise leicht die Nase vorn und liefert das Resultat schon nach 0,13 Sekunden. Im 64-Bit-Modus braucht das Programm 0,14, mit –m32 –mfpmath=sse oder –mx32 0,16 Sekunden.

In der letzten Testrunde ging es darum, den mathematischen Bibliotheksroutinen auf den Zahn zu fühlen, vertreten durch sqrt, hypot, exp und cos sowie deren mit float rechnenden Gegenstücken. Erwartungsgemäß fällt der Performance-Unterschied zwischen x32- und 64-Bit-Modus gering aus; lediglich die Exponentialfunktion exp liefert ihr Ergebnis bei x32 und double-Arithmetik etwa 6 % schneller. Gegenüber dem klassischen 32-Bit-Modus mit FPU hat x32 immer die Nase vorn: Im Wurzelziehen um 12,8 (double) und 12,4 % (float), beim Pythagoras (hypot) um 13,6 und 35,3 %, bei der Exponentialfunktion um 57,1 und 40,9 % sowie beim Cosinus um 94,7 und 35,5 %.

Unterm Strich hält das x32-ABI, was es verspricht: Integer- und FP-Rechenleistung wie im 64-Bit-Modus ohne den Nachteil des erhöhten Speicherbedarfs für Zeiger. Für viele Anwendungen passt das genau – Speicherfresser wie Datenbanken, optimierende Compiler, Bildbearbeitungsprogramme oder manche wissenschaftlichen Anwendungen ausdrücklich ausgenommen. Auf den meisten Linux-Desktops ist jedoch der Browser das speicherhungrigste Programm; kommt der nicht mit 4 GByte RAM aus, haben die Entwickler etwas falsch gemacht.

Dass sich die Macher von Linux-Distributionen noch nicht auf x32 gestürzt haben, dürfte an der Mehrarbeit liegen, die damit verbunden ist: Statt zwei Programmversionen sind nun drei zu erstellen, zu verwalten und zu warten. Obendrein müssen einige Programme und Bibliotheken portiert werden, etwa weil sie Inline-Assembler verwenden. Die klassischen i386-Versionen mit 32-Bit-Kernel aufgeben kann man nicht, solange noch Prozessoren im Gebrauch sind, die den 64-Bit-Modus nicht beherrschen. Für modernere Rechner wäre die beste Variante ein Hybridsystem mit 64-Bit-Kernel, bei dem die speicherhungrigen Applikationen im 64-Bit-Modus laufen und die übrigen das x32-ABI nutzen.

Alle Links: www.ix.de/ix1305076 (mr)