1 Punkte von GN⁺ 2024-01-06 | 1 Kommentare | Auf WhatsApp teilen

Schnelleren Code schreiben: Übergeben Sie auf AMD64 keine Strukturen, die größer als 16 Byte sind

  • Zur Verbesserung der Performance der Sprache Neat wurde die Übergabe eines Arrays als einzelner Strukturparameter auf die Übergabe von drei Zeigerparametern umgestellt.
  • Der Grund, warum Neat-Arrays langsamer waren als Arrays in der Sprache D, war, dass das 24 Byte große Array die Grenze von 16 Byte überschritt und Parameter deshalb auf andere Weise übergeben wurden.
  • Laut der SystemV-AMD64-ABI-Spezifikation werden alle Strukturen, die größer als 16 Byte sind, über Zeiger übergeben.

Bestätigung des Problems durch Benchmarks

  • Durch Benchmarks wurde der Performance-Unterschied zwischen der Übergabe einer Struktur und der Übergabe einzelner Felder bestätigt.
  • Bei der Übergabe einer Struktur sind Zuweisung auf dem Stack und ein Kopiervorgang erforderlich, während einzelne Felder direkt über SSE-Register übergeben werden.
  • Die Übergabe einzelner Felder ist etwa doppelt so schnell wie die Übergabe einer Struktur.

Die Entscheidung von Sprachdesignern

  • Beim Aufruf einer C-API muss das C-ABI eingehalten werden, intern verwendete High-Level-Typen müssen jedoch nicht als Strukturen dargestellt werden.
  • Sprachdesigner können festlegen, wie Arrays, Tupel und Summentypen übergeben werden.
  • Die Übergabe von Typen, die größer als 16 Byte sind, als einzelne Felder kann zur Performance-Steigerung beitragen.

Meinung von GN⁺

  • Dieser Artikel ist für Entwickler, die sich für Software-Optimierung interessieren, sehr aufschlussreich.
  • Insbesondere zeigt er, dass bei der Entwicklung performancekritischer Anwendungen die Größe von Strukturen und ihre Übergabeform einen wichtigen Einfluss haben können.
  • Sprachdesigner oder API-Entwickler können diese Informationen nutzen, um Möglichkeiten zur Performance-Verbesserung zu finden.

1 Kommentare

 
GN⁺ 2024-01-06
Hacker-News-Kommentare
  • Im Zusammenhang mit dem SysV-amd64-ABI kann das interne ABI einer Sprache auf etwas anderes als SysV gesetzt werden. Solange es nicht gegenüber einem SysV-C-Aufrufer offengelegt wird, kann jede gewünschte Calling Convention verwendet werden. Der Unterschied bei NeatLang scheint deutlich komplexer zu sein als nur die LLVM-Calling-Convention zu ändern, und der Autor möchte Typen gegenüber C-Programmen möglicherweise mit einer festen Calling Convention offenlegen.
  • Es fehlt oft an Verständnis für die Kosten der Argumentübergabe, und der dazu geschriebene Artikel ist aufschlussreich. Bei Google etwa erscheint die Praxis, 24-Byte-Objekte per Wert zu übergeben, nicht im Profiler, verursacht aber in jeder Funktion Kosten.
  • Beim Umstieg auf x64 wurde eine Grafik-Engine benchmarked, weil man sich Sorgen machte, dass ein vec3-Objekt (3xfloat) von 12 Byte auf 16 Byte anwächst. Dabei stellte sich heraus, dass die Verwendung von 16 Byte schneller ist, weil sie auf 8-Byte-Lesezugriffe ausgerichtet ist. Infolgedessen wird vec3 wie vec4 verwendet. Es wird empfohlen, immer das Gesamtsystem zu benchmarken.
  • Bereits in Register geladene Argumente sind performanter als Stack-Schreibzugriffe, und Stack-Manipulationen sind schneller als Heap-Allokationen. Das ist ein Grund, warum komplexer Code mit vielen globalen Variablen schnell laufen kann, während elegante rekursive Funktionen oder Tuple-/Struct-/Listen-Argumente langsamer sind. Ersterer lässt sich leicht zu dichten Assembler-Schleifen optimieren.
  • In MSVC werden Structs mit mehr als 8 Byte über den Stack übergeben. Das ist ein ABI-Detail, auf das man sich in portablem Code nicht verlassen sollte. Bei selten aufgerufenen Funktionen sollte man sich jedoch nicht zu sehr stressen, und bei häufig aufgerufenen kleinen Funktionen sollte man dem Compiler erlauben, den Code zu inlinen, wodurch nützlichere Optimierungen aktiviert werden als nur die Übergabe von Argumenten in Registern.
  • Unter Windows werden bei Verwendung der Standard-cdecl-Calling-Convention Structs größer als 8 Byte nicht in Registern übergeben.
  • Auf amd64 ist es mit dem SysV-amd64-ABI langsam, Structs größer als 16 Byte per Wert zu übergeben und zurückzugeben, aber es ist oft den Gewinn an Codeklarheit wert. Natürlich trifft das hier nicht zu, aber man kann etwa innerhalb einer eigenen Sprache ein benutzerdefiniertes ABI verwenden, wie es z. B. jeder C++-Compiler, Golang, OCaml und SBCL tun.
  • In C++ gibt es als Faustregel, Nicht-Primitive-Typen per Referenz (oder falls nötig per Pointer) zu übergeben, sofern es keinen guten Grund dagegen gibt. Das liegt auch am ABI und dient dazu, Kopier- oder Move-Konstruktoren zu vermeiden. Wer Performance optimieren will, muss in C++ auf solche langweiligen Low-Level-Details achten.
  • Der Artikel verlinkt auf einen sehr speziellen Benchmark, in dem Java (JIT) schneller ist als C++ und sogar schneller als Scala. Es werden Fragen aufgeworfen, was Julia HO ist und warum es so schnell ist, warum der Geschwindigkeitsunterschied zwischen Python und Pypy groß ist, ob es Gründe gibt, Pypy nicht zu verwenden, und ob es zum Standard werden sollte.
  • Im gezeigten Beispiel lässt sich das ändern, ohne den Aufrufer zu beeinflussen, indem der Parametertyp struct Vector so angepasst wird, dass stattdessen per Referenz als const struct Vector & übergeben wird. Viel C++-Code mit Pointer-Bugs hat Pointer unnötig verwendet, obwohl eine Übergabe per Referenz einfacher und sicherer gewesen wäre.