Cacheorientierte Programmierung / Geschwindigkeitsverbesserung durch Cache

  • Gruß zur späten Stunde.

    Aufgrund aktuellen Anlasses im Studium wollte ich mal ein kleines Snippet los werden, welches die Cache orientierte Programmierung nahe bringt.
    Grundmodell ist das Transponieren einer Matrix.
    Die native Lösung wäre folgende:

    Code
    transposedMatrix[y][x] = Matrix[x][y];


    Wir können die Matrix tatsächlich auf keinem anderen Weg rotieren, so oder so müssen wir sie in dieser Art und Weise zuordnen. Allerdings können wir dabei effektiv den Cache nutzen.
    Hier erstmal das Snippet (in Java):

    JavaCode


    CacheTest.java

    Java
    public class CacheTest {
        public static void main(String[] args) {
            matrixTester test = new matrixTester();
            System.out.println("No Cache: " + (test.rotateMatrix() * 1e-6));
            System.out.println("Cache: " + (test.rotateMatrixCache() * 1e-6));
            System.out.println("No Cache Check: " + test.checkMatrixRightRotatedNoCache());
            System.out.println("Cache Check: " + test.checkMatrixRightRotatedCache());
        }
    }

    matrixTester.java

    Nun - Ich nutze meinen Cache selbstverständlich nicht optimal. Ohne die Specs meines Prozessors anzuschauen habe ich einfach mal die Annahme getroffen, dass eine Cache-Line 16 * 32 Bit lang ist (== 64 Byte). Um das genau heraus zu bekommen hätte ich ein Script gebraucht, welches mir mit verschiedenen Messungen sagen kann, wie groß eine Line meines L1 Caches tatsächlich ist.
    Eventuell schreibe ich noch Eines zum Messen und stelle das ebenfalls hier rein.
    Ferner nutze ich in dem Snippet auch nicht die Blockgröße aus. Zwar nutze ich die 64 Byte pro Cache-Line, allerdings beziehe ich mich überhaupt nicht auf die Anzahl der Cache Lines (in dem Falle die Anzahl der Zeilen, also y), das würde zusätzlich noch ein Speedup herbei bringen.

    Nun aber zum eigentlichen Inhalt des Ganzen: Es geht dabei darum, zu zeigen, dass man effektiver Speicher(zugriffe) macht, und somit in seinen Programmen die Laufzeit verkürzt. Primär ist eine Matrix Rotation das perfekte Anwendungsbeispiel, da wir nach und nach jeden Wert in den Speicher laden, ihn zuweisen, und ihn dann nicht mehr anrühren.
    Allerdings ist die native Implementierung nicht gerade zeitgünstig. Wir befüllen den Cache immer wieder mit neuen Werten, sodass wir keinerlei Zeitgewinn gegenüber dem Zugriff auf den RAM haben. Es wird etwas in den Cache geladen, einmal angerührt, und schon wird das nächste nachgeladen.
    Dabei hat unser Cache eine weit schnellere Zugriffszeit als unser Arbeitsspeicher (für die Details empfehle ich Wikipedia - Cache). Deshalb ist es sinnvoll, wenn wir im Cache nur genau die Werte betrachten, die bereits darin stehen.
    Beim laden einer Zeile der Matrix wird genau so viel der Zeile in den Cache geladen, wie ab index i hinein passt (in dem Falle: 16 mal ein 32 Bit Integer, was äquivalent zu 64 Byte Speicher ist (n Bit / 8 = x Byte). Wenn wir nun allerdings permanent die Zeilen durchrödeln, statt bei der x-ten Stelle weiter zu springen, wird der Cache nicht fortlaufend belegt, sondern "irgendwie". Wie genau das funktioniert, das weiß nur der Hersteller (oder jemand, der davon tatsächlich Ahnung hat), Fakt ist allerdings, dass er nicht mehr nur die Cache Lines belegen wird.
    Es ist also ratsam, dass man in solchen Fällen den Cache nur bis dahin nutzt, wo er voll geschrieben ist. Das schafft im Endeffekt einen enormen Geschwindigkeitsvorteil.

    Wie enorm ist denn der Vorteil?
    Nun - In dem Beispiel von mir komme ich mit meinem i5-2500 auf folgende Ausgaben, ausgegeben in Millisekunden:

    No Cache Check & Cache Check beziehen sich lediglich darauf, ob die Matrix richtig rotiert wurde. Wenn dies nicht der Fall ist, dann hätten wir ja nix gewonnen.
    Warum der erste Wert so hoch ist weiß ich jetzt nicht, im Allgemeinen ist aber ein Geschwindigkeitsvorteil um den Faktor ~4.5 zu erkennen. Bei genügend großen Daten macht das durchaus schon mal was aus.


    Anbei noch die Bemerkung, warum der Code in Java ist: Ich versuchte in AutoIt ähnliches umzusetzen, ziemlich genau 1:1 den Code, jedoch mit weitaus kleineren Array Größen (AutoIt tut sich bei 4096*4096 im Minutenbereich weh). Allerdings erzielte ich in der Cache Lösung sogar eine langsamere Zeit, als in der NoCache Lösung, was sich mir absolut nicht erschloss. Vielleicht habe ich irgendwas falsch geschrieben, jemand Anderes kann gerne versuchen das in AutoIt umzusetzen und dann mitzuteilen. Vorab wäre mein Schluss für AutoIt daraus, dass der Interpreter "Sowieso macht, was er will", aber die Alteingesessenen wissen bestimmt, woran es liegt.

    Damit Allen ein schönes Wochenende!

    Es gibt sehr viele Leute, die glauben. Aber aus Aberglauben.
    - Blaise Pascal

  • Hi,
    sehr schönes Beispiel der von mir schon zig-fach angesprochenen SoA (Structure of Arrays) vs. AoS (Array of Structures) - "Problematik" (cache pollution). :klatschen:
    Einfach mal AoS SoA in google eingeben zeigt div. Anwendungsbeispiele von Beschleunigungen bis zu Faktor 4-5, nur weil bspw. in einer Schleife über Array [ i ][j] anstatt über Array[j] [ i ] iteriert wird.
    Übrigens sind aktuelle (Intel-)Compiler in der Lage, dahingehend zu optimieren..., die Zeiten, um aus Schei** Gold zu machen, sind also angebrochen. Wieso "Programmierer" diese imho elementaren Grundlagen von "Programmierung" nicht beherrschen bzw. nicht berücksichtigen (können) darf sich jeder selbst überlegen.

    Wieso das bei Autoit nicht funktioniert ist auch klar. Die Verarbeitung/Initialisierung eines Arrays erfolgt nicht in einer wie in anderen Sprachen definierten Struct mit fest zugewiesenem Speicherbereich, sondern über Zeiger auf die einzelnen Elemente. Ich persönlich liebe diese den Datentyp "Variant", wird dadurch doch das schnelle und einfache Programmieren unterstützt. Auf Kosten der Geschwindigkeit, aber das nehme ich gern in Kauf!
    Weiterhin ist der Overhead des Interpreters so hoch, dass einige eingesparte Prozessortakte pro Befehl völlig im Grundrauschen der Ausführung verschwinden.
    Siehe Beispiele von mir bzgl. optimiertem Sieb des Eratosthenes im µ-It zur Primzahlermittlung. Der 1:1 von AutoIt nach PowerBasic übersetzte Code läuft dort mit Faktor 5 schneller als der ursprüngliche Code. In AutoIt läuft der "schnellere" Algorithmus ca. 3x LANGSAMER, weil der Interpreter 20 Zeilen mehr Code in der Schleife verarbeiten muss....

    Wenn ich "schnell" programmieren möchte, suche ich den "inner loop" und schreibe diesen in Assembler (oder jeder anderen Compilersprache) und rufe per DllCall() diese optimierten Routinen auf. Und da merkt man schon bei relativ kleinen Berechnungen bspw. einfachen Filtern (Grafiken mit Full HD ~ 2.1 Megapixel) den Einfluss des Cache, wenn man die Schleife statt von 0 bis Ende, nun von Ende bis 0 läuft. Die im Speicher stehende Bitmap ist nämlich "Bottom-up", d.h. das letzte Pixel steht an der ersten Speicherstelle.

    Noch krasser lässt sich der Einfluss des Cache bei Zugriff auf un(mis)aligned Speicherstellen beobachten, da schiebt der Prozessor bzw. Cachecontroller Straftakte ein, von Zugriffen von Bereichen, die sich über die Cacheline-Grenzen bewegen, ganz zu schweigen.

    Mittlerweile wird die Hardware an das Unvermögen der Programmierer/Programmiersprachen/Bibliotheken angepasst, neuere Prozessoren "bestrafen" un(mis)aligned Zugriffe nicht mehr, da wird "einfach" für jeden "dämlichen" Zugriff ein Szenario fest verdrahtet, fertig ist die Laube. Der Prozessor wird größer/teurer/energiehungriger, was solls... :Face:

    Wen das Thema interessiert, der sollte google quälen mit "agner fog cache optimization"

  • Hi Andy,

    Zitat


    Array [ i ][j] anstatt über Array[j] [ i ] iteriert

    Dabei muss ich gerade daran denken, dass mein Beispiel auf Anderen Rechnern auch voll in die Hose gehen könnte (wobei ich das heutzutage nicht mehr glaube). Angenommen er würde die Daten in den Cache über die Zeile, statt über die Spalte pre-fetchen, so hätten wir absolut 0 Zeitgewinn gegenüber dem gezeigten Beispiel.

    Zitat


    wenn man die Schleife statt von 0 bis Ende, nun von Ende bis 0 läuft. Die im Speicher stehende Bitmap ist nämlich "Bottom-up"


    Ebenfalls das mit der Bitmap - Wenn man vorher weiß, wie die Daten im Speicher liegen, dann kann man wunderbar den Zugriff umschreiben.

    Gebe dir absolut Recht, dass es heute noch viel zu viele Leute gibt, die sich ihre Daten (leider) nicht anschauen, bevor sie damit arbeiten. Und sich dann wundern, warum das Script wie n Karren Mist vom Zeitumfang ist.
    Anbei noch zu AutoIt: Ich habe sowas in die Richtung schon vermutet, allerdings erschließt sich mir der Nutzen dahinter nicht. Da wir in AutoIt weder dynamisch das Array vergrößern / verkleinern können, ohne es zu überschreiben, liegt da in meinen Augen keine Notwendigkeit für.

    Es gibt sehr viele Leute, die glauben. Aber aus Aberglauben.
    - Blaise Pascal

  • Angenommen er würde die Daten in den Cache über die Zeile, statt über die Spalte pre-fetchen, so hätten wir absolut 0 Zeitgewinn gegenüber dem gezeigten Beispiel.

    naja, genau darum geht es ja^^

    Ein schönes Beispiel ist eine 32-Bit-Bitmap mit einzelnen Pixeln, welche ja bekanntermaßen aus den 4 Farbkomponenten BGRA bestehen. Wieso das so ist, wird immer das sahnige Geheimnis der Entwickler bleiben, genauso hätte man 4 einzelne Bitmaps mit jeweils den einzelnen Farb-Komponenten zusammenpacken können.
    Für jeden Farbfilter wäre diese Variante ein Traum, jeden Farbkanal einzeln bearbeiten ist ungleich schneller, man hat 4x so viele Farbinformationen im Cache wie mit der BRGA-Variante! Und das alles mit dem Hintergedanken von Multithreading....Statt bei FullHD 4 mal (je Farbkanal) durch 2MegaPixel zu rennen, bleibt man lokal je Thread in 500MB (je Farbe). Gerade relativ kleine Caches profitieren enorm!
    Bleibt zu erwähnen, dass ich genau dieses schon programmiert habe. Aufwendige Filter um Faktor 2-3 zu beschleunigen ist keine Kunst, wenn man "nur" die Ausgangsdaten ins "richtige" Format bringt!
    Btw. machen genau das auch hochoptimierte Bibliotheken, nicht nur zur Bildbearbeitung!

    "Früher" habe ich mich auch noch für prefetching interessiert, also das vorausschauende Laden von Speicher in den Cache, bin aber schnell auf den Trichter gekommen, dass das bei neueren Prozessoren kontraproduktiv ist ||

    Daher bin ich ein großer Fan des Intel-Compilers, nichts ist besser als eine Software, die WEISS, wie man einen (Intel-)Prozessor RICHTIG anspricht, so das dessen volle Leistungsfähigkeit ausgenutzt wird. Und Leistung kostet, was solls :thumbup:

  • AMD A6-3400M APU (Laptop-Quadcore)

    - einmal "normal" getaktet @1.4 Ghz
    No Cache: 1180.726944
    Cache: 398.540398
    No Cache Check: true
    Cache Check: tru

    - getaktet @2.3 Ghz
    No Cache: 682.7804259999999
    Cache: 255.043579
    No Cache Check: true
    Cache Check: true

    - untertaktet @350 Mhz
    No Cache: 4248.2123169999995
    Cache: 814.006177
    No Cache Check: true
    Cache Check: true

    was auffällt ist, das je höher der Prozessor getaktet ist, der Cache weniger zur Beschleunigung beiträgt! Jedenfalls bei meinem Prozessor.
    Anders herum im Vergleich zu voll übertaktetem Prozessor (2300 vs 350Mhz)
    Mit einem Sechtel an Takt läuft der Code mit Cache-Beschleunigung relativ gesehen nur 3x so langsam!!!
    Je Takt ist das doppelt so "schnell" wie ein voll übertakteter Prozessor!

    Einfach ausgedrückt, je weniger Prozessortakt, desto besser die Cache-Performance bzw. desto deutlicher der "Gewinn" durch den Cache! Genau aus diesem Grund wurden anno Tobak überhaupt Prozessorcaches erfunden :D

  • Das ist ja auch "klar" (wie Dozenten immer so schön zu sagen pflegen). Wenn der Prozessor langsamer taktet, die Zugriffszeit auf den RAM jedoch gleich bleibt, gehen "unterm Strich" noch mehr Takte verloren.
    Angenommen er kann alle 2 Takte was aus dem RAM holen und jeden Takt aus dem Cache, hat aber nur 10 Hz, dann ist die Zugriffszeit für 5 Daten aus dem RAM eben 10t. Da man die Daten noch verarbeitet und den RAM währenddessen nicht nutzt, kann man eben in den Cache prefetchen und hat danach für alle weiteren Operanden eine Zugriffszeit für 5 Daten aus dem (nun) Cache eben 5t.
    Mal ganz laienhaft modelliert :rofl:

    Es gibt sehr viele Leute, die glauben. Aber aus Aberglauben.
    - Blaise Pascal

  • Genau so ist der Ablauf!
    Deshalb sollte man auch darauf achten, so viele Daten wie möglich "lokal" also im Sinne von einem eng begrenzten Bereich zu bearbeiten.
    Es bietet sich an, Daten entweder in sog. "Tiles" oder "Chunks" anzuordnen (idealerweise so groß, dass diese komplett in den L1-Cache passen!), um die Beschleunigung durch den Cache voll auszunutzen. Imho ist der Zugriff auf Daten im Lvl1-Cache 1 Takt, während aus dem RAM je nach Zugriffszeit der Chips mehrere Straftakte auflaufen!
    Auch den eigentlichen Programmcode sollte man so lokal wie möglich halten, wildes Umhergespringe veranlaßt den Prozessor jedes mal die Sprungvorraussagen neu zu berechnen bzw. die Pipelines für die Befehlsfolgen neu zu füllen.

    Btw. könntest du, um mal die Wirksamkeit des Caches zu demonstrieren, ein Beispiel schreiben, bei dem einzelne Bytes im Abstand von 65 Bytes gelesen (und geschrieben!!) werden!
    Wieso gerade 65 Bytes? Weil die Cachelines bei modernen Prozessoren 64 Bytes lang sind und somit immer neu geladen werden müssen, da definitiv ein Sprung über die nächste Grenze geht! Wenn dann noch das Byte wieder zurück in den Speicher geschrieben wird, tritt der SuperGAU auf :D
    Bei 513 Bytes kotzt auch der Lvl2-Cache, so kann man sein Programm locker um Faktor 10 verlangsamen, ohne Sleeps() einzubauen :rofl:
    Übrigens kann man sein eigenes Programm sehr einfach mit dem Task-Manager prüfen. Werden dort viele Cache-Misses angezeigt, sollte man aktiv werden^^

    ciao
    Andy


    "Schlechtes Benehmen halten die Leute doch nur deswegen für eine Art Vorrecht, weil keiner ihnen aufs Maul haut." Klaus Kinski
    "Hint: Write comments after each line. So you can (better) see what your program does and what it not does. And we can see what you're thinking what your program does and we can point to the missunderstandings." A-Jay

    Wie man Fragen richtig stellt... Tutorial: Wie man Script-Fehler findet und beseitigt...X-Y-Problem

    Einmal editiert, zuletzt von Andy (16. Mai 2016 um 15:05)

  • Hi Andy,
    das probier ich glatt mal am nächsten Wochenende. Bin bis Freitag (leider) noch mit lernen beschäftigt, aber die Idee ist gut :rofl:

    Es gibt sehr viele Leute, die glauben. Aber aus Aberglauben.
    - Blaise Pascal