1 Punkte von GN⁺ 2024-07-30 | 1 Kommentare | Auf WhatsApp teilen
  • Ein Experiment, das beim Kopieren von Strings ASCII-Großbuchstaben mit AVX-512-BW in 64-Byte-Blöcken in Kleinbuchstaben umwandelt, um SIMD-Leistung auch bei kleinen Strings herauszuholen
  • Der Kern der Implementierung ist eine Maskenoperation, die nur an den Positionen 'a' - 'A' addiert, an denen jedes Byte zwischen 'A' und 'Z' liegt
  • Kurze Strings und die verbleibenden Endstücke langer Strings werden mit masked load/store verarbeitet, um die bei SIMD-Code häufigen Kosten für kleine Reststücke zu senken
  • Gemessen auf Clang 16, Debian 11 und einem AMD Ryzen 9 7950X bei etwa 1MiB Kopiermenge in 1-Byte- bis 1KiB-Chunks gehört tolower64 im Vergleichsfeld durchgehend zu den schnelleren Varianten
  • Auf Zen 4 zeigte sich AVX-512-BW als gut passend für String-Verarbeitung, während ARM SVE und die RISC-V Vector Extension nicht direkt im Detail überprüft werden konnten

Mit AVX-512-BW ein 64-Byte-tolower() bauen

  • Ziel ist es, einen tolower()-Kernel per SIMD zu implementieren, der beim Kopieren eines Strings ASCII-Großbuchstaben in Kleinbuchstaben umwandelt
  • AVX-512-BW ist eine Erweiterung, die Operationen auf Byte- und Word-Ebene unterstützt und auf aktuellen AMD-Zen-Prozessoren genutzt werden kann
    • AVX-512 ist in mehrere Erweiterungen aufgeteilt, was die Unterstützung komplex macht
    • Besonders auf Intel-Seite gilt die Unterstützung als uneinheitlich
  • ARM SVE bietet ebenfalls masked load/store auf Byte-Ebene, was gut zur String-Verarbeitung passt
    • Verfügbar auf aktuellen großen ARM-Neoverse-Kernen, etwa bei Amazon Graviton
    • Auf Apple Silicon ist es nicht verfügbar
  • Die RISC-V Vector Extension folgt einem ähnlichen Stil wie ARM SVE und ist auf mehreren kleinen Single-Board-Computern nutzbar

Funktionsweise von tolower64()

  • tolower64() ist ein AVX-512-basierter Kernel, der 64 Byte auf einmal verarbeitet
  • Zuerst werden Referenzwerte in ein Vektorregister mit 64 Bytes geladen
    • 'A'
    • 'Z'
    • 'a' - 'A'
  • Der Eingabezeichenvektor c wird mit 'A' und 'Z' verglichen, um jeweils 64-Bit-Masken zu erzeugen
    • Positionen, an denen c >= 'A' gilt
    • Positionen, an denen c <= 'Z' gilt
  • Beide Masken werden mit _kand_mask64() kombiniert, um eine is_upper-Maske zu erzeugen, die nur Positionen mit Großbuchstaben markiert
  • Abschließend wird _mm512_mask_add_epi8() angewendet
    • Für Bytes, bei denen is_upper false ist, bleibt der ursprüngliche Wert c erhalten
    • Für Bytes, bei denen is_upper true ist, wird daraus c + ('a' - 'A')

Verarbeitung langer und kurzer Strings

  • Der Großteil langer Strings wird mit gewöhnlichem unaligned Vector load/store verarbeitet
    • _mm512_loadu_epi8()
    • tolower64()
    • _mm512_storeu_epi8()
  • Für kurze Strings und die letzten verbleibenden Stücke langer Strings wird masked unaligned load/store verwendet
  • Die Maske wird so erzeugt, dass nur die unteren len-Bits gesetzt sind
    • uint64_t len_bits = (~0ULL) >> (64 - len)
    • Übergabe an ein SIMD-Maskenregister mit _cvtu64_mask64(len_bits)
  • _mm512_maskz_loadu_epi8() füllt Positionen mit deaktivierter Maske im Zielregister mit 0
  • _mm512_mask_storeu_epi8() speichert nur an Positionen mit aktivierter Maske
  • Dieser Ansatz ist der Schlüssel für die schnelle Verarbeitung kleiner String-Fragmente

Benchmark-Bedingungen und Vergleichskandidaten

  • Der Benchmark lief mit Clang 16, Debian 11 und einem AMD Ryzen 9 7950X
  • Gemessen wurde eine Kopiermenge von etwa 1MiB, wobei die Chunk-Länge von 1 Byte bis 1KiB variiert wurde
  • Um unterschiedliche Ausrichtungen von Quell- und Ziel-String zu berücksichtigen, wurde zwischen den Strings ein Abstand von einigen Bytes eingefügt; diese Bytes wurden nicht in die 1MiB-Messmenge einbezogen
  • Da der Ryzen 9 7950X pro Kern 1MiB L2-Cache hat, ist zu erwarten, dass jeder Testlauf bis in den L3-Cache hineinreicht
  • Jede Funktion wurde separat kompiliert, um Interferenzen durch Inlining und Codebewegung zu vermeiden
    • In realem Code ist es allerdings wahrscheinlicher, dass man Inlining eher fördert als verhindert

Ergebnis: die gleichmäßige Leistung von tolower64

  • Das pinke tolower64 liegt insgesamt durchgehend nahe an den schnellsten Testfunktionen
    • Bei einer Länge von 65 Byte gibt es einen kleinen Einbruch beim Übergang zum zweiten Vektor
    • Danach steigt die Leistung schnell wieder an, ohne tiefe Leistungstäler, was zeigt, dass masked load/store bei kurzen String-Stücken effektiv ist
  • Das grüne copybytes64 ist eine memcpy-Variante, die AVX-512 auf ähnliche Weise nutzt
    • Es ist nicht wesentlich schneller als tolower64
    • Aktuelles Clang erkennt die Bedeutung dieser Funktion und schreibt sie vollständig um, daher wurde sie mit Clang 11 kompiliert
  • Das orange copybytes1 ist eine Byte-für-Byte-memcpy-Variante
    • Kompiliert mit Clang 11
    • Zeigt, dass die Auto-Vektorisierungs-Heuristik von Clang 11 bei String-Stücken unter 256 Byte relativ schwach ist
  • Das rote tolower ist die langsame Baseline, die das Standard-tolower() aus <ctype.h> aufruft
  • Das violette tolower1 ist ein Byte-für-Byte-tolower(), kompiliert mit Clang 16
    • Die Auto-Vektorisierung von Clang 16 ist deutlich besser als bei Clang 11
    • Es bleibt langsamer als die handgeschriebene Version und erzeugt wesentlich komplexeren Code
    • Die Verarbeitung kurzer String-Stücke ist nicht so gut wie bei tolower64, weshalb der Leistungsgraph spitz und unruhig verläuft
  • Das braune tolower8 ist das SWAR-tolower() aus dem vorherigen Beitrag
    • Clang versucht zwar Auto-Vektorisierung, aber wegen der Komplexität der Funktion fällt das Ergebnis schwach aus
    • Obwohl mit Clang 16 kompiliert, zeigt sich ein Leistungseinbruch bei 256 Byte im Stil von Clang 11
  • Das blaue memcpy ruft memcpy aus glibc auf
    • Anfangs ist es schnell, fällt dann aber in einen Bereich auf etwa die Hälfte der Geschwindigkeit von copybytes64
    • Die Ursache konnte nicht geklärt werden

Fazit und Code

  • AVX-512-BW eignet sich sehr gut für Strings, insbesondere für kurze Strings
  • Auf Zen 4 ist es sehr schnell, und die Intrinsic-Funktionen sind vergleichsweise leicht zu verwenden
  • Das auffälligste Merkmal ist die gleichmäßige Leistung
    • Die Leistungstäler, die bei Auto-Vektorisierung durch den Wechsel auf Skalarcode für kleine String-Stücke entstehen, sind kaum zu sehen
  • Mangels bequemem Zugang zu Hardware mit ARM-SVE- oder RISC-V-Vector-Extension-Unterstützung konnten diese beiden Erweiterungen nicht im Detail untersucht werden
  • Der Code ist im Git-Repository der Website verfügbar

1 Kommentare

 
GN⁺ 2024-07-30
Hacker-News-Kommentare
  • Der Trick „unsafe read beyond of death“ gilt im Speichermodell von Rust und LLVM als undefiniertes Verhalten, selbst wenn die Hardware ihn zulässt.
    Wie bei anderem undefiniertem Verhalten kann der Compiler bei der Optimierung davon ausgehen, dass „so etwas nicht passiert“, was zu unerwarteten Ergebnissen führen kann; um das zu umgehen, muss man Inline-Assembly verwenden.
    https://github.com/ogxd/gxhash/issues/82

    • Es wäre schön, wenn es für solche Fälle eine Alternative zu Assembly gäbe.
      Ein Load nach dem Muster „Werte außerhalb des Allokationsbereichs werden als unbestimmte Elemente gelesen, und nur wenn die Hardware es nicht mag, ist es undefiniertes Verhalten“ scheint nicht schwer zu unterstützen zu sein; intern könnte er auch einfach ein Alias für den entsprechenden Assembly-Aufruf sein.
      Noch besser wäre es, wenn nach jeder Allokation — malloc, Stack, Konstanten usw. — mindestens etwa 64 Byte an fault-freien Adressen garantiert wären, aber das wäre deutlich komplizierter, weil mehrere Komponenten zusammenarbeiten müssten.
      Bei einem Custom Allocator ist das trivial, aber dann lässt sich SIMD-Code nur schwer auf Daten außerhalb des eigenen Heaps anwenden, und man bleibt an eine winzige Möglichkeit eines Segfaults gebunden.
      Auch Sanitizer oder Valgrind wären weiterhin nützlich, weil Werte außerhalb des Bereichs als unbestimmte Werte verfolgt werden können und erst bei tatsächlicher Verwendung einen Fehler auslösen können.
    • Ich frage mich, ob das selbst auf Hardware-Ebene wirklich stimmt.
      Mich würde interessieren, was passiert, wenn man aus einer nicht gemappten Page oder geschütztem Speicher liest; da ich den Code nicht gesehen habe, weiß ich nicht, ob die Alignment-Garantien das verhindern.
    • Die Erklärung „wenn es undefiniertes Verhalten ist, kann der Compiler davon ausgehen, dass es nicht passiert“ ist falsch.
      Undefiniertes Verhalten ist ein Fachbegriff aus dem C-Standard, daher ist schon die Verallgemeinerung merkwürdig; ANSI C erlaubt eine solche Annahme nicht ausdrücklich, und ISO C ist zwar offener, rechtfertigt diese Annahme aber auch nicht konkret.
      Erklärungen der Art „UB = der Compiler darf annehmen, dass es nicht passieren kann“ halte ich für ziemlich unehrliche Panikmache.
  • Beim Blick auf den sauberen und performanten Code im Artikel frage ich mich, wie AMDs AVX512-Implementierung und Intels geplantes AVX10 miteinander konkurrieren werden.
    Der Kern von AVX10 scheint darin zu liegen, Intels P-Core/E-Core-Situation zu lösen; AMD scheint dagegen je nach Situation mit der Vollbreiten-Implementierung in Zen 5 oder der 256-Bit-Zweifachverarbeitung in Zen 4 und Zen-5-Mobile einen besseren Ansatz zu wählen, während die API nahtlos bleibt.
    Auch die großen Performance-Gewinne im Artikel stammen alle von Zen-4-Kernen, und weil AVX512 viele Vorteile hat, ist es frustrierend, dass Intel es durch Marktsegmentierung so stark eingeschränkt und damit die Einführung in allgemeinem Client-Code praktisch verhindert hat.

    • Wenn Intel künftig tatsächlich AVX10/256 in alle neuen CPUs einbaut, wird es am Ende wegen der Verbreitung gewinnen.
      Der Markt hat Codepfad-Verzweigungen pro CPU immer wieder abgelehnt, und bei wichtigen SIMD-Implementierungen zählt in der Praxis der kleinste gemeinsame Nenner.
      AVX10.1/256 und AVX512VL haben eine gemeinsame Teilmenge; wenn genug Zeit vergangen ist und die meisten CPUs sie unterstützen, werden die Leute genau darauf abzielen.
      AMD wird weiterhin einfache Siege in einigen Benchmark-Apps holen, die für AVX512 aktualisiert wurden, aber wenn Intel an seinem AVX10-Plan festhält, wird AMD vermutlich ebenfalls flächendeckend auf eine zweifach verarbeitende SIMD-Pipeline setzen, um AVX10/256 effizient zu unterstützen und zugleich AVX512-Kompatibilität zu behalten.
      Intel hat in den letzten zehn Jahren viele schlechte Entscheidungen getroffen, aber den Markt über den Befehlssatz zu fragmentieren war eine der schlimmsten. Damit haben sie den Schwung und das Interesse an neueren Innovationen selbst abgewürgt; Funktionen wie Maskenoperationen sind viel wichtiger als die Breite an sich, daher hoffe ich, dass AVX10/256 in die gesamte Produktpalette kommt.
    • Zen 4s AVX512-Implementierung ist nicht double-pumped, und Technikjournalisten sollten aufhören, sie so zu nennen.
      Der Begriff hat eine bestimmte Bedeutung, die nicht zum tatsächlichen Verhalten passt.
      Zen 4 dekodiert ZMM-Registeroperationen lediglich in mehrere Mikro-Operationen und schedult sie auf freie 256-Bit-Einheiten; Full-Width-512-Bit-Shuffles werden durch dedizierte Hardware speziell behandelt, um teure Emulation zu vermeiden.
      Deshalb verhält sich Zen 4 mit seinen vier 256-Bit-SIMD-Einheiten wie ein leistungsfähiger 2×512-Bit-Kern, und diese Implementierung ist keineswegs eine billige Lösung, sondern dürfte die bisher beste Form in Consumer-Hardware sein.
    • Ich verstehe nicht, warum Intel das Problem nicht löst, indem es den E-Cores double-pumped AVX512 gibt. Oder sie bauen für Desktops einfach CPUs nur mit P-Cores, so wie es eigentlich sein sollte.
      Sie hatten bereits jahrelang Zeit, das zu beheben, und es ist ärgerlich, dass die Unterstützung durch AMD wegen des Marktanteils trotzdem nicht zur Adoption führt; AVX10 wird Intel leider wohl ermöglichen, die Welt noch länger auszubremsen.
      Auf dem Desktop möchte ich bessere Kerne, mehr Kerne und einen gut standardisierten Befehlssatz sehen, der nützliche Funktionen wie breites SIMD, float16 und gather/scatter freischaltet; AMD macht das ziemlich gut.
      Intel dagegen setzt schwache Kerne neben ordentliche Kerne, beschneidet die ordentlichen Kerne, um zu den schwachen zu passen, bringt über mehrere Generationen CPUs mit derselben Kernzahl heraus, lässt es durch schwache Kerne so aussehen, als gäbe es viele Kerne, veröffentlicht so viele Befehlsvarianten, dass sich kaum ein nützlicher gemeinsamer Nenner bilden kann, und lässt sogar die Befehlssatzunterstützung fallen, die sie selbst in Aussicht gestellt hatten.
      Meine Präferenz bei Desktop-Herstellern war in den 90ern Intel, Anfang der 2000er AMD, Ende der 2000er und in den 2010ern Intel, und jetzt wieder AMD. Ich bin gespannt, was Intel tun wird, um außer der Behinderung des Gegners wieder Boden gutzumachen; Konkurrenz muss weiter bestehen, damit sich keine Seite zu sehr ausruht.
  • Material, das man sich zum Spaß anschauen kann: http://www.unicode.org/Public/3.1-Update1/CaseFolding-4.txt

    • So etwas gibt es auch: Wenn man das deutsche ß in Großbuchstaben umwandelt, ändert sich die String-Länge
      Zum Beispiel wird "straße".upper() zu 'STRASSE'
      Außerdem geht ohne Angabe einer Locale bei der Hin- und Rückumwandlung des punktlosen i aus Turksprachen in Groß-/Kleinbuchstaben etwas kaputt: 'ı'.upper().lower() wird zu 'i'
    • Zum Glück stammt dieser Code aus DNS-Arbeiten und ist daher nur ASCII, sodass man sich mit dieser Komplexität nicht befassen muss
      Es gibt mehrere ASCII-Protokolle ohne Beachtung von Groß-/Kleinschreibung, und sie tauchen häufig in den Hot Paths vieler Server auf
    • Strings, die intern wie IDs verwendet werden, sind etwas anderes als von Menschen eingegebener Text
      Für Erstere reicht normalerweise reines ASCII in einer 8-Bit-Codierung, Letzteres wird kompliziert
      DNS-Adressen sind ein einfaches Beispiel: Technisch können sie fast jedes Unicode-Zeichen enthalten, für die tatsächliche DNS-Auflösung werden sie aber in eine sehr eingeschränkte ASCII-Teilmenge umgewandelt, und dieser Auflösungsprozess unterscheidet nicht zwischen Groß- und Kleinschreibung
      Natürlich gibt es auch Programmiersprachen, die alle Schriftsysteme von Unicode unterstützen und deren Identifier trotzdem nicht zwischen Groß- und Kleinschreibung unterscheiden. Falls du damit zu tun hast: mein Beileid
    • Im Zusammenhang mit dem Beispiel, dass das deutsche maße zu MASSE wird: Im Deutschen gibt es auch das große Eszett
      Es ist noch nicht weit verbreitet und es gibt nur wenige unterstützende Fonts, aber theoretisch existiert es inzwischen
  • Ich vermute, die Erklärung zur „Maskenaddition“ im Artikel ist falsch
    Müsste nicht addiert werden, wenn is_upper false ist, und unverändert kopiert werden, wenn es true ist?

    • Ah, mir ist nachträglich klar geworden, dass der Variablenname to_upper verkehrt herum ist und eigentlich to_lower heißen sollte
      Danke, dass du auf die verwirrende Stelle hingewiesen hast; Artikel und Code habe ich korrigiert
    • Diese Operation ist tolower
      Großes A ist 0x40, Kleinbuchstaben liegen bei 0x60, daher muss das Addieren von 0x20 passieren, wenn is_upper true ist
  • Solche SWAR-Optimierungen sind oft nur dann nützlich, wenn der String an einer 8-Byte-Adresse ausgerichtet ist
    Wendet man SWAR-Algorithmen auf nicht ausgerichtete Strings an, werden sie häufig langsamer als der ursprüngliche Algorithmus
    Teilt man das Ganze in drei Schritte auf – Anfang bis zur ausgerichteten Adresse verarbeiten, den ausgerichteten Hauptteil verarbeiten und den Rest von weniger als 8 Byte verarbeiten –, kommen noch mehr Instruktionen hinzu
    Einen ähnlichen Fall mit Benchmarks zur falschen Behauptung, utf8.IsValid in Go sei schneller, gibt es hier: https://github.com/sugawarayuuta/charcoal/pull/1

    • Die Masken-SIMD-Operationen von AVX-512 und ARM SVE sind genau dafür gedacht, dieses Problem zu lösen
      Speicheroperationen bleiben immer ausgerichtet und haben die volle Vektorgröße, aber man kann nur die gültigen Elemente maskieren
      Selbst wenn eine maskierte Vektor-Speicheroperation nicht ausgerichtet ist und über nicht gemappte oder geschützte Pages hinweggeht, tritt kein Fault auf, solange die betreffende Lane durch die Maske deaktiviert ist
      Für Operationen wie strlen(), bei denen die Länge vorher nicht bekannt ist, gibt es außerdem spezielle Load-Instruktionen, die die Vektorlänge direkt vor dem ersten Element verkürzen, das einen Fault auslösen würde
  • Maskierte Addition sieht elegant aus. Es wäre schön, wenn man in .NET-Intrinsics die Maskenregister von AVX512 direkt manipulieren könnte, aber derzeit muss man sich auf „erkannte Idiome“ verlassen.
    Analysiert man die vom Autor mit GCC erzeugte Kernschleife mit uiCA (CQA/MAQAO) für Ice Lake, kommt man auf etwa 32 B/Zyklus; auf 3 GHz umgerechnet wären das fast 96 GiB/s, wenn man annimmt, dass es keinen Speicher-Flaschenhals gibt. Natürlich ist bei solchen Algorithmen der Speicherzugriff immer der Flaschenhals.
    Ganz nah an optimaler Ausnutzung scheint es allerdings nicht zu sein; mit Clang kommt man durch besser aufgelöstes Unrolling und eine bessere Instruktionsauswahl auf 42,67 B/Zyklus. Auch der L2-Cache dürfte es schwer haben, einen solchen Durchsatz zu halten, aber interessant ist schon, dass die Groß-/Kleinschreibung von Strings mittlerer Länge ungefähr in der Zeit fertig ist, in der das Licht des Bildschirms die Hornhaut erreicht.
    Vor ein paar Monaten habe ich in C# etwas Ähnliches für ASCII-Groß-/Kleinschreibung innerhalb von UTF-8 implementiert: https://github.com/U8String/U8String/blob/main/Sources/U8Str...
    Da kurze Strings die meisten Codebasen dominieren, ist eine entrollte Konvertierung für Längen unterhalb der Vektorlänge wichtig, und switch wird zu einer Sprungtabelle mit branchless Fall-through kompiliert.
    Derzeit nutze ich nur bis 256 Bit, weil das bei CPUs wie Zen 3 oder 4, die nur 256×4-SIMD-Einheiten haben, bereits sättigt. Einen direkten Vergleich mit der C-Version gibt es hier: https://godbolt.org/z/eTGYhTPan
    Unter AVX512 sollte mit vpternlogd wohl auch eine Umwandlung mit 3 Instruktionen möglich sein; als ich AVX512-Hardware nutzen konnte, hat .NET das mit 256-Bit-Breite + AVX512VL so optimiert, aber derzeit lässt sich das merkwürdigerweise nicht mit 512-Bit-Breite reproduzieren.
    Man sieht dort auch einen fehlgeschlagenen SWAR-Versuch auf der switch-Dispatch-Seite; ich frage mich, welche Lizenz der Artikel hat. Wenn es die Testsuite besteht, würde ich es gern übernehmen.

    • Clang und GCC unterscheiden sich darin, wie sie Intrinsics behandeln, und besonders bei AVX-512-Instruktionen weicht Clang eher als GCC von den im Intel-Guide angegebenen Opcodes und Algorithmen ab.
      Wenn man die Struktur der beiden Compiler bedenkt, ist das nachvollziehbar, aber manchmal verbessert es das Ergebnis und manchmal verschlechtert es es.
      Vor ein paar Jahren habe ich an einem stark vektorisierten Projekt gearbeitet, das mit beiden compiliert werden musste, und am Ende hielten wir neben der C-Referenzversion Inline-Assembly für bestimmte Targets und .S-Dateien im Repository vor.
      Das Makefile wurde unübersichtlich, und weil sogar Benchmarks in die Testsuite mussten, war der Wartungsaufwand hoch. Daraus habe ich geschlossen, dass man sehr vorsichtig sein sollte, Intrinsics als Low-Level-Mittel zu verwenden, das besser sein soll als Auto-Vektorisierung.
      Beispiel: Unter https://godbolt.org/z/T4Pjhrz5d war die GCC-Ausgabe wie erwartet, die Clang-Ausgabe dagegen überraschend und tatsächlich langsamer. In einer Schleife sind es laut uiCA 7 Zyklen gegenüber 4 Zyklen bei GCC, und das zeigte sich auch in einem realen App-Benchmark, in dem diese Funktion in einem Brute-Force-Algorithmus Milliarden Mal ausgeführt wird.
      Beim Blick in die LLVM-Codebasis erinnere ich mich auch an ein Problem, bei dem Clang 16 wegen eines internen Refactorings manche maskierten AVX-512-Instruktionen unter Umständen gar nicht ausgeben konnte.
    • Die Analyse ist sehr hilfreich.
      Ich hatte nicht auf maximal mögliche Performance gezielt; anfangs wollte ich nur sehen, ob es überhaupt funktioniert, und dass der erste Versuch ziemlich gut ausfiel, war ein Bonus.
      Mein Hauptinteresse gilt Strings, die kürzer als ein Vektorregister sind, und dem Entfernen der Täler im Durchsatzdiagramm.
      Wenn man dem Code-Link am Ende des Blogposts folgt, findet man die Lizenzinformationen; abgesehen von dem ursprünglich für BIND geschriebenen MPL-2.0-Teil ist es 0BSD oder MIT-0.
    • Aus einem großen Assembly-Block allein erkennt man es schlecht, aber Clang schreibt (x >= 'a' && x <= 'z') in die Form (x - 'a') < ... um und spart damit eine Instruktion.
      Wegen merkwürdiger Opcode-Encoding-Details kann in manchen Fällen sogar ein Register-Load entfallen.
  • Ich weiß nicht, was swar ist.

    • Es ist die Abkürzung für „SIMD Within A Register“.
      Gemeint ist üblicherweise eine Technik, bei der man mehrere Elemente in ein einzelnes Register packt und es auch ohne explizite SIMD-Instruktionen faktisch wie SIMD nutzt.
      Wenn man zum Beispiel in ein 64-Bit-Register eine 31-Bit- und eine 32-Bit-Zahl packt und 1 Bit für den Carry übrig lässt, kann man mit einer einzigen 64-Bit-Addition zwei Additionen ausführen.
      In Spielen wurden solche Tricks für Grafik verwendet, indem RGB(A)-Werte in 32-Bit-Integer gepackt wurden; auch ScummVM hat Code, der zwei 16-Bit-RGB-Pixel innerhalb eines 32-Bit-Werts interpoliert, also insgesamt 6 Komponenten: https://github.com/scummvm/scummvm/blob/master/graphics/scal...
    • Es bedeutet SIMD innerhalb eines Registers.
  • Seit Unicode aufgekommen ist, ist das Konzept von Groß- und Kleinbuchstaben zu einem Sumpf geworden.
    Wenn man es richtig machen will, braucht man viele Daten.
    Wenn man an einer Aufgabe arbeitet, bei der es davon abhängt, ob ASCII-tolower rechtzeitig fertig wird, sollte man besser etwas Grundlegendes am Spielfeld und an den Voraussetzungen ändern.

  • Ich habe früher einmal schwarze Ränder um Bilder gelegt, um das Problem von SIMD-Lesezugriffen über das Pufferende hinaus vollständig zu vermeiden.
    Das funktionierte sehr gut, und in Sachen Geschwindigkeit konnte es einige OpenCV-Implementierungen schlagen, aber man hat die Eingaben nicht immer so vollständig unter Kontrolle.

  • Ich frage mich, ob es schon so ausprobiert wurde. Das Ergebnis der Auto-Vektorisierung sieht ziemlich sauber aus.
    https://godbolt.org/z/1c5joKK5n

    • Das ist im Grunde dasselbe wie tolower1. Siehe die Bulletpoints unter dem Diagramm.