UNIXwork

Raspberry Pi1 vs Raspberry Pi4 vs Fujitsu s920 vs Sun Ultra 45

24. December 2019

Vor Jahren hatte ich ein Raspberry Pi 1B als Server für ein paar kleine Dienste im Einsatz. Ersetzt habe ich das Teil aber später durch einen Fujitsu Futro s920, ein Thin-Client mit einer AMD G-Serie Quad-Core CPU (GX-415 GA). Die AMD G-Serie ist für embedded Computer entwickelt und daher hat der Fujitsu Thinclient auch eine recht niedrige Leistungsaufnahme. Zumindestens im Idle ist diese nicht viel höher als beim Raspberry Pi, aber dafür mit deutlich mehr Rechenleistung.

Ein Raspberry Pi 4 bietet ebenfalls eine Quadcore-CPU mit ähnlichem Takt. Daher hatte ich die Idee, mal ein bisschen die Hardware zu benchmarken. Und aus Spaß vergleiche ich das noch mit einer Sun Ultra 45 Workstation, die zumindestens einen ähnlichen CPU-Takt hat. Diese hat zwei UltraSPARC IIIi CPUs, ich glaube aus dem Jahr 2003.

Test-Hardware

Sowohl beim Raspberry Pi 1 als auch 4 kommt ein 32bit Raspbian zum Einsatz. Vielleicht nicht optimal für die Performance, auf grund des wenigen RAMs (2 GB) aber praktisch vielleicht die bessere Wahl für den Produktivbetrieb.

Benchmarks

Als Integer-Benchmark wird die Schach-Engine stockfish benutzt. Stockfish bietet sehr gute Optimierung für x86 und auch ARM, aber nicht für sparc. Daher habe ich das ganze ohne spezielle Architekturoptimierungen kompiliert, also nur mit normaler Compiler-Optimierung.

stockfish bench 128 <num-cpus>

Als Floatingpoint-Benchmark kommt c-ray zum Einsatz. Dies ist ein einfacher in C geschriebener Raytracer.

c-ray-mt -t <num-cpus> -s 1000x1000 -r 10 -i scene -o out.ppm

Ergebnisse Stockfish

Die Ausgabe von stockfish bench sieht so aus:

Total time (ms) : 14304
Nodes searched  : 5607358
Nodes/second    : 392013

Für den Vergleich nehm ich nur Nodes/second. Je größer hier der Wert ist, desto besser:

rpi1     u45-1cpu   u45-2cpu   rpi4-1cpu   rpi4-4cpu   s920-1cpu   s920-4cpu
----------------------------------------------------------------------------
26503    277715     559029     392013      1421621     403146      1595700
26655    277386     559604     393222      1402173     402682      1530797
26623    277002     549077     385544      1424797     403261      1580684
26303    277152     566223     391739      1458121     402191      1551561
26926    277317     548266     391411      1435041     403146      1503410

Mittelwert mit Standardabweichung:

rpi1           26.602   ±    203,68
u45-1cpu      277.314,4 ±    240,66
u45-2cpu      556.439,8 ±  6.832,99
rpi4-1cpu     390.785,8 ±  2.691,36
rpi4-4cpu   1.428.350,6 ± 18.298,75
s920-1cpu     402.885,2 ±    400,12
s920-4cpu   1.552.430,4 ±  3.293,49

Ergebnisse C-Ray

C-Ray liefert am Ende die Anzahl der Millisekunden, die das Rendern gedauert hat.

rpi1     u45-1cpu   u45-2cpu   rpi4-1cpu   rpi4-4cpu   s920-1cpu   s920-4cpu
----------------------------------------------------------------------------
88444    29585      16133      10015       3128        13699       4282
86406    29013      16112      10028       3118        13698       4189
87455    29014      16134      10032       3138        14068       4186
86456    29000      16106      10032       3119        14058       4189
86676    28998      16033      10004       3110        13701       4185

Mittelwert mit Standardabweichung:

rpi1        87.087,4 ± 775,59
u45-1cpu    29.122   ± 231,59
u45-2cpu    16.103,6 ±  37,01
rpi4-1cpu   10.022,2 ±  11,03
rpi4-4cpu    3.122,6 ±   9,58
s920-1cpu   13.844,8 ± 178,19
s920-4cpu    4.206,2 ±  37,93

Auswertung

Der Unterschied zwischen Raspberry Pi 1 und 4 ist gigantisch. Allein im Single-Core Benchmark ist ein rpi4 14 mal schneller als ein rpi1. Dafür bietet ein rpi4 aber statt einem Core gleich 4. Auch die FP-Geschwindigkeit ist deutlich höher. Aber das war auch nicht anders zu erwarten.

Spannender ist der Kampf zwischen rpi4 und Fujitsu s920. Diese sind in etwa auf Augenhöhe. Im Integer-Benchmark führt die AMD-CPU, im FP-Benchmark die ARM-CPU. Interessanterweise ist beim Stockfish-Benchmark die Standardabweichung beim s920 deutlich geringer als beim rpi4. Eine mögliche Ursache könnten die thermischen Probleme beim rpi4 sein, die zu einer Reduzierung des CPU-Takts führen könnten. Doch bei C-Ray ist es genau umgekehrt. Ich habe noch keine Erklärung dafür, wieso beim rpi4 hier die Werte so viel stabiler sind, als bei den anderen Testkandidaten.

Etwas schwach finde ich die Resultate bei der Ultra 45. Die Hardware ist natürlich schon etwas in die Jahre gekommen, aber da es sich hier nicht um kleine embedded CPUs handelt, hätte ich doch mehr erwartet. Eine Ursache könnte aber auch sein, dass ich auf der Ultra 45 ein recht altes OS (Solaris 10) betreibe und zum kompilieren hatte ich nur gcc 5.5 zur Verfügung. Auf dem rpi4 z.B. kam gcc 8.3 zum Einsatz.

Das war jetzt ein einfacher CPU-Benchmark. Interessant wäre ein IO-lastiger Benchmark. Die Ultra 45 wird nicht kampflos aufgeben. Gerade die Bus-Architektur ist beim rpi4 immer noch Schrott, daher könnte die Ultra 45 hier punkten. Fortsetzung folgt (irgendwann).

Autor: Olaf | 0 Kommentare | Tags: benchmark, rpi, sun, fujitsu, x86, arm, sparc

Executable Memory und Intel XED

24. December 2016

Ich hab mich schon öfter gefragt, was man alles tun muss, um einen JIT-Compiler zu schreiben. Oder anders gefragt, wie kann man zur Laufzeit Maschinencode generieren und ausführen.

Zunächst einmal benötigt man Speicher, der es überhaupt erlaubt, dass davon Code ausgeführt werden kann. Wenn man mit malloc Speicher alloziert, ist dieser nicht ausführbar. Intern verwendet malloc mmap und das kann auch einfach direkt genutzt werden. Dabei kann man direkt die Zugriffsrechte für den Speicher festlegen. Man könnte sie allerdings auch im nachhinein mit mprotect ändern. Mit mmap werden auch Dateien in den Speicher gemapped, durch das Flag MAP_ANONYMOUS liefert der Kernel aber ganz ohne Datei den gewünschten Speicher.

void *execmem = mmap(NULL, len, PROT_EXEC | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);

Jetzt haben wir ausführbaren Speicher. Wenn dort Instructions reingeladen werden, können diese ausgeführt werden. Mein erster Versuch war einfach mit memcpy von einem Function-Pointer dort ein paar Bytes rein zu kopieren. Das hat zwar funktioniert, auch wenn ich nicht wusste wie viele Bytes die Funktion eigentlich groß ist und ich einfach eine größere Menge kopiert habe, aber das wäre für diesen ohnehin schon etwas hackigen Blogartikel etwas zu unsauber.

Glücklicherweise hat Intel kürzlich eine Bibliothek für das decoden und encoden von x86-Maschinencode veröffentlicht. Damit ist es mir gelungen nur die Instructions der Funktion zu kopieren.

Um den Maschinencode zu decoden braucht man erstmal einen Pointer auf den Code, in meinem Fall einen Funktions-Pointer.

const xed_uint8_t *inst = (const xed_uint8_t*)func;

Einen Befehl decoden macht folgender Code:

xed_decoded_inst_t dec;
xed_error_enum_t error;

// init xed_decoded_inst_t struct
memset(&dec, '\0', sizeof(xed_decoded_inst_t));
xed_decoded_inst_set_mode(&dec, XED_MACHINE_MODE_LONG_64, XED_ADDRESS_WIDTH_64b);

// decode instruction
error = xed_decode(&dec, inst, 15);

Dies liest 15 Bytes (die Maximalgröße einer Instruction) und dekodiert die Instruction. Wie groß diese dann ist, kann mit xed_decoded_inst_get_length abgefragt werden. Mit der Länge kann man dann zur nächsten Instruction springen.

Man kann diese auch als Assembler-String formatieren:

// print instruction
xed_format_context(XED_SYNTAX_ATT, &dec, buffer, 1024, 0, 0, 0);
printf("%s\n", buffer);

Hier ist das fertige Beispielprogramm. Kompiliere ich das ohne Optimierung und führe es aus ist die Ausgabe:

$ ./x86dec 
copy function code:

pushq  %rbp
mov %rsp, %rbp
movl  %edi, -0x4(%rbp)
movl  %esi, -0x8(%rbp)
movl  -0x8(%rbp), %eax
movl  -0x4(%rbp), %edx
add %edx, %eax
popq  %rbp
retq  

execute new code:

f(10, 50) = 60

Das ganze erfüllt jetzt natürlich keinen Zweck. Spannend wird es erst, wenn man eigenen Code generiert und den dann ausführt.

Was man noch erwähnen sollte ist, dass CPUs separate Data- und Instruction-Caches haben. Glücklicherweise muss man sich bei x86-CPUs keine Sorgen darüber machen, da dort erkannt wird, wenn Speicher modifiziert wird, der gerade auch im Instruction-Cache ist. Hingegen bei RISC-Architekturen, z.B. ARM, muss meistens der Instruction-Cache manuell aktualisiert werden.

Siehe auch: Self-Modifying Code and the Cache on Intel x86

Autor: Olaf | 0 Kommentare | Tags: c, x86, asm, jit