Einleitung: Warum "denkt" der Computer schneller als der Programmierer?
\n\nStellen Sie sich vor: Sie haben einen Abend damit verbracht, die Collatz-Folge für die Zahl 27 auf ein Blatt Papier zu schreiben. Und dann hat Ihr Kollege eine C++-Schleife geschrieben, die in einer Sekunde eine Milliarde Zahlen durchläuft. Sind Sie wütend? Warten Sie, das ist erst der Anfang.
\n\nIm Jahr 2025 ist auf Stack Overflow eine alte, aber sehr aufschlussreiche Frage neu aufgeflammt: "Why does C++ code for testing the Collatz conjecture run faster than hand-written assembly?". Man sollte meinen, Assembler sei der reine Dialog mit dem Prozessor, ohne Vermittler. Wie kann es sein, dass ein hochsprachlicher C++-Code Assembler übertrifft, der von einem Menschen geschrieben wurde?
\n\nLassen Sie uns dieses Paradoxon am Beispiel der Collatz-Vermutung (auch bekannt als 3n+1-Problem) analysieren. Sie werden erfahren, warum moderne C++-Compiler wahre Zeitmaschinen sind und wie man im Jahr 2025 Code schreibt, der schneller ist als die "manuelle" Registerverwaltung.
\n\nWas ist die Collatz-Vermutung und was hat das mit C++ zu tun?
\n\nZur Erinnerung: Man nehme eine beliebige natürliche Zahl n. Ist sie gerade, teilt man sie durch 2. Ist sie ungerade, multipliziert man sie mit 3 und addiert 1. Die Vermutung besagt, dass jede Zahl früher oder später bei 1 ankommt. Die Aufgabe des Programmierers ist es, dies für einen möglichst großen Zahlenbereich zu überprüfen.
Eine klassische Implementierung in C++ sieht so aus:
\n\n#include <iostream>\n\nint collatz_steps(long long n) {\n int steps = 0;\n while (n != 1) {\n if (n % 2 == 0) {\n n /= 2;\n } else {\n n = 3 * n + 1;\n }\n ++steps;\n }\n return steps;\n}\n\nint main() {\n long long max_steps = 0;\n long long best_num = 0;\n for (long long i = 1; i <= 1000000; ++i) {\n int s = collatz_steps(i);\n if (s > max_steps) {\n max_steps = s;\n best_num = i;\n }\n }\n std::cout << "Zahl: " << best_num << " Schritte: " << max_steps;\n}\n\n\nSieht eigentlich nicht kompliziert aus. Aber genau dieser einfache Code hat eine legendäre Debatte mit Assembler ausgelöst.
\n\nWarum verliert Assembler? Die Hauptfehler im Jahr 2025
\n\nWenn ein Entwickler in Assembler schreibt, denkt er oft: "Ich weiß, wie der Prozessor funktioniert, ich schreibe es perfekt." Aber moderne Prozessoren sind nicht einfach Befehlsausführer. Sie sind komplexe superskalare Pipelines mit Sprungvorhersage, spekulativer Ausführung und Cache-Speicher. Der C++-Compiler (GCC, Clang, MSVC) weiß alles über seine Zielarchitektur.
\n\nFehler Nr. 1: Ignorieren der Verzweigungsoptimierung
\n\nIm manuellen Assembler schreibt der Programmierer Verzweigungen oft "geradeheraus":
\n; Pseud-Assembler\ncheck_even:\n test rax, 1\n jnz is_odd\n shr rax, 1\n jmp next\nis_odd:\n mov rbx, rax\n shl rax, 1\n add rax, rbx\n inc rax\nnext:\n ...\n\nDer C++-Compiler mit dem Flag -O3 macht es raffinierter. Er verwendet CMOV (conditional move) und Bit-Magie. Statt Verzweigung kommt Arithmetik:
; GCC 14 wird so etwas generieren:\n mov rcx, rax\n shr rcx, 1 ; durch 2 teilen (wenn gerade)\n and eax, 1 ; niederwertigstes Bit prüfen\n lea rdx, [rax + rcx*2 + 1] ; raffinierte Berechnung für ungerade\n test eax, eax\n cmove rax, rcx ; wenn gerade - Division nehmen\n cmovne rax, rdx ; wenn ungerade - 3n+1 nehmen\n\n\nErgebnis: Keine Pipeline-Stalls aufgrund falsch vorhergesagter Sprünge. Im Jahr 2025 ist dies kritisch, da der Sprungvorhersager in der CPU zwar intelligenter geworden ist, aber für Zufallszahlen immer noch nicht perfekt ist.
\n\nFehler Nr. 2: Vektorisierung vergessen (AVX-512)
\n\nModerne C++-Compiler (insbesondere Clang 18+ und GCC 14+) vektorisieren Schleifen automatisch. Wenn Sie eine einfache Prüfschleife schreiben, kann der Compiler bis zu 8 Zahlen pro Takt verarbeiten, wobei er