joggel hat geschrieben:Da muss ich gleichnochmal nachfrage:
Angenommen, ich hätte Bildinformationen im Speicher, RGB, jeweils char (1Byte).
Lustig, weil ich die Frage nur stelle, weil ich auch Bilddaten möglichst performant durchboxen will.
joggel hat geschrieben:Also wäre es performanter, wenn ich die Bilddaten als jeweils 1Integer bzw. short pro Kanal speichere?
Performanter für
was? Ein komplexer Kompressionsalgorithmus verhält sich völlig anders als die Konvertierung von Bilddaten von RGB zu BGR …
Generell gilt, dass die Datenmenge so klein wie möglich zu halten ist. Jedes Mal, wenn die CPU auf Daten zugreift, die noch nicht im Cache sind, muss sie leer laufen – heute zwischen 100 und 400 Takte lang, was im Vergleich zu der Verarbeitungszeit der meisten Daten gigantisch ist. Machst du deine Pixel ohne Nutzwert doppelt oder viermal so groß indem du für die Kanäle
short oder
int benutzt, läuft deine CPU auch ohne Nutzen doppelt bis viermal so lange leer, um all die Daten zu laden (auch, wenn der Verlust in der Realität schwächer ausfällt, weil moderne CPUs die Speicherlatenz einigermaßen verstecken bzw anderweitig nutzen (HyperThreading) können).
Jetzt weiter zu ausgerichteten Daten: die CPU kann Speicher am schnellsten laden, wenn die Adresse des Elements glatt durch seine Größe teilbar ist – vier Bytes große
ints sollten also immer auf einer durch vier teilbaren Adresse liegen, 128-Bit-
float4-Vektoren sollten immer auf einer durch 16 teilbaren Adresse liegen usw. Grund dafür ist der Aufbau der Speichereinheit.
Über den Performance-Nachteil nicht ausgerichteter Daten gibt es widersprüchliche Aussagen. Manche sagen, er falle nicht ins Gewicht; andere sprechen von einer Halbierung der Lade-Performance. Fakt ist: Lädt die CPU z.B. ein
int, das auf einer Adresse mit 2 am Ende liegt, muss es die
ints bei den Adressen 0 und 4 laden, per
0x0000FFFF und
0xFFFF0000 maskieren, die Bits herumschieben und per bitweisem OR zusammenführen, bevor die Zahl im Register landet. Allzweck-Architekturen wie x86 haben dafür oft eigene Schaltkreise; bei IA-64 muss die CPU das „manuell“ erledigen und dann dauert es schonmal viermal so lang wie ein ausgerichteter Zugriff.
Darüber musst du dir übrigens keine großen Sorgen machen: Der Compiler sorgt dafür, dass die meisten lokalen Variablen und Klassenattribute (durch Padding) ausgerichtet sind; außerdem ist der Speicher, den
new und
malloc() zurückgeben, immer 8-Byte-ausgerichtet.
Jetzt zu dem, was Aramis sagte – Alphakomponente (oder generell ein Füllbyte) hinzufügen. Vorher lagen deine Pixel so im Speicher:
RGBRGBRGBRGBRGBR
0123456789ABCDEF
Jetzt so:
RGBxRGBxRGBxRGBx
0123456789ABCDEF
Vorher haben deine RGB-Tripel an beliebigen Adressen gestartet (weil die Vielfachen von 3 durch so ziemlich jede andere Zahl teilbar sind). Jetzt starten sie ausschließlich an Adressen, die durch vier teilbar sind. Anstatt dass du drei Kanäle einzeln laden und zusammenfügen musst (oder das die CPU erledigen lässt) kannst du ein Tripel als
int adressieren und schnell laden. Wenn ausgerichtetes Laden und die Logikvereinfachung 50 % bessere Performance bewirken, aber das Füllbyte 33 % mehr Cache-Misses provoziert, sind wir immernoch bei einer Verbesserung.
Es geht aber noch weiter: Wenn du weißt, dass das Bild eine durch vier teilbare Breite hat, kannst du immer vier Pixel auf einmal laden – und zwar als drei
ints, die jeweils wieder ausgerichtet sind, weil du das Bild quasi in 12er-Pakete zerstückelst:
RGBRGBRGBRGB RGBRGBRGBRGB RGBRG…
0123456789AB CDEF01234567 89ABCDE…
(Beachte, dass ein Block jeweils bei 0, 4, 8 oder C – also einer durch vier teilbaren Adresse – anfängt.)
Du lädst also drei ausgerichtete Register aus dem Speicher in die CPU. Angenommen, wir sind immernoch bei der RGB-zu-BGR-Konvertierung, so kannst du die jetzt munter Bits dieser Register herumschieben, vertauschen oder die Daten auf vier Register mit jeweils RGB entpacken und wieder zusammenführen und letztendlich an die Zieladresse schreiben.
Hier haben wir jetzt 50 % bessere Performance dadurch, dass wir nur ausgerichtete Daten laden, und zwar
ohne die zusätzlichen Cache-Misses, weil die Datenmenge genauso groß bleibt wie zuvor.
Es gibt aber immernoch Raum für Verbesserungen. Wie gesagt, Speicherbandbreite zum Cache ist ein Problem. Bei der Konvertierung von Bilddaten haben wir eine ganze besondere Charakteristik: Was wir einmal von der Quelladresse laden, packen wir nie wieder auch nur mit der Kneifzange an. Ebenso kehren wir niemals zu den Daten zurück, die wir an die Zieladresse geschrieben haben.
Der Cache verhält sich aber genau so: Zur Hälfte wird er mit dem gefüllt, was wir in letzter Zeit gelesen haben, und zur anderen Hälfte mit dem, was wir in letzter Zeit geschrieben haben. Das können wir
komplett umkrempeln: Der Cache soll
nichts beinhalten als die Daten, die wir demnächst verarbeiten
werden, damit sich die Ladezeit vom Hauptspeicher auf null reduziert.
Leider bietet C++ keine Sprachmittel dafür an, aber: Der SSE-Anweisungssatz stellt uns die
MOVNTI(MOVe Non-Temporal Integer)-Anweisung zur Verfügung. Die lädt Daten aus dem RAM,
ohne, dass sie im Cache landen und kann umgekehrt auch CPU-Register in den Speicher schieben, ohne, dass sie den Cache passieren. Gleichzeitig bietet der Befehlssatz einige
PREFETCH-Anweisungen, die die CPU anweisen, den Cache mit Daten an einer gegebenen Adresse zu füllen. Streuen wir diese Anweisung so ein, dass sie Speicher ein paar KiB vor der Adresse, die wir momentan verarbeiten, referenziert, liegen diese Bilddaten schon fertig im Cache, sobald sie unsere Konvertierung erreicht. Im besten Fall limitiert unsere Konvertierung dann nur noch der RAM-Durchsatz (das wären bei einem Core i7 12 GiB/s vom RAM zur Northbridge, wenn ich mich recht irre – oder eine Bitmap von 65536×65536 RGB-Pixeln pro Sekunde).
Auf Schrompfs Empfehlung bin ich auch gestern erst wieder gestoßen … für mich ist sie aber ungeeignet, weil die Daten irgendwann auch wieder auf dem Bildschirm oder in einer Datei landen müssen – und die erwarten RGB. Das Zusammenführen würde wieder bedeutend länger brauchen, als man durch das Trennen vorher gespart hat.