Codeoptimierung: Speicher

    Die meisten Programmierer betrachten ein Computersystem als einen Prozessor, der Anweisungen ausführt, und einen Speicher, der Anweisungen und Daten für den Prozessor speichert. In diesem einfachen Modell wird der Speicher durch ein lineares Array von Bytes dargestellt, und der Prozessor kann in konstanter Zeit überall im Speicher zugreifen. Obwohl dies für die meisten Situationen ein effektives Modell ist, spiegelt es nicht wider, wie moderne Systeme tatsächlich funktionieren.

    Tatsächlich bildet das Speichersystem eine Hierarchie von Speichergerätenmit unterschiedlichen Kapazitäten, Kosten und Zugriffszeiten. Prozessorregister speichern die am häufigsten verwendeten Daten. Kleine schnelle Caches in der Nähe des Prozessors dienen als Pufferzonen, in denen ein kleiner Teil der Daten in einem relativ langsamen RAM gespeichert wird. RAM dient als Puffer für langsame lokale Laufwerke. Lokale Festplatten dienen als Puffer für Daten von Remotecomputern, die über ein Netzwerk verbunden sind.

    Bild

    Die Speicherhierarchie funktioniert, weil gut geschriebene Programme dazu neigen, häufiger auf den Speicher auf einer bestimmten Ebene zuzugreifen als auf den Speicher auf einer niedrigeren Ebene. Daher kann die Speicherung auf einer niedrigeren Ebene langsamer, größer und billiger sein. Als Ergebnis erhalten wir eine große Speichermenge, die die Speicherkosten ganz unten in der Hierarchie hat, aber Daten mit der Geschwindigkeit der schnellen Speicherung ganz oben in der Hierarchie an das Programm liefert.

    Als Programmierer müssen Sie die Speicherhierarchie verstehen, da dies die Leistung Ihrer Programme stark beeinträchtigt. Wenn Sie verstehen, wie das System Daten in der Hierarchie nach oben und unten verschiebt, können Sie Programme schreiben, die ihre Daten höher in der Hierarchie platzieren, damit der Prozessor schneller darauf zugreifen kann.

    In diesem Artikel untersuchen wir, wie Speichergeräte in einer Hierarchie organisiert sind. Wir werden uns insbesondere auf den Cache-Speicher konzentrieren, der als Pufferzone zwischen Prozessor und RAM dient. Dies hat den größten Einfluss auf die Programmleistung. Wir werden das wichtige Konzept der Lokalität vorstellen , lernen, wie man Programme auf Lokalität analysiert, und Techniken lernen, die dazu beitragen, die Lokalität Ihrer Programme zu erhöhen.

    Dieser Artikel wurde vom sechsten Kapitel in Computersysteme: Die Perspektive eines Programmierers inspiriert . In einem anderen Artikel dieser Reihe, "Code-Optimierung: Der Prozessor" , kämpfen wir auch um Prozessorzyklen.

    Gedächtnis ist auch wichtig


    Betrachten Sie zwei Funktionen, die die Elemente einer Matrix zusammenfassen. Sie sind fast identisch, nur die erste Funktion umgeht die Elemente der Matrix zeilenweise und die zweite in Spalten.

    int matrixsum1(int size, int M[][size])
    {
        int sum = 0;
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                sum += M[i][j];    // обходим построчно
            }
        }
        return sum;
    }
    int matrixsum2(int size, int M[][size])
    {
        int sum = 0;
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                sum += M[j][i];    // обходим по столбцам
            }
        }
        return sum;
    }
    


    Beide Funktionen führen die gleiche Anzahl von Prozessoranweisungen aus. Auf einer Maschine mit einem Core i7 Haswell ist die erste Funktion für große Matrizen 25-mal schneller . Dieses Beispiel ist eine gute Demonstration dieser Speicher auch ankommt . Wenn Sie die Wirksamkeit von Programmen nur anhand der Anzahl der auszuführenden Anweisungen bewerten, können Sie sehr langsame Programme schreiben.

    Daten haben eine wichtige Eigenschaft, die wir Lokalität nennen . Wenn wir an den Daten arbeiten, ist es wünschenswert, dass sie sich im Speicher in der Nähe befinden. Das Umgehen einer Matrix durch Spalten hat eine schlechte Lokalität, da die Matrix zeilenweise im Speicher gespeichert ist. Wir werden unten über die Lokalität sprechen.

    Hierarchie der Erinnerung


    Ein modernes Speichersystem bildet eine Hierarchie von schnellen kleinen Speichertypen bis zu langsamen großen Speichertypen. Wir sagen, dass eine bestimmte Hierarchieebene zwischengespeichert wird oder ein Cache für Daten ist, die sich auf einer niedrigeren Ebene befinden. Dies bedeutet, dass es Kopien von Daten von einer niedrigeren Ebene enthält. Wenn der Prozessor einige Daten empfangen möchte, sucht er zuerst auf den schnellsten hohen Ebenen danach. Und es geht zu niedrigeren, wenn es nicht finden kann.

    An der Spitze der Hierarchie stehen Prozessorregister. Der Zugriff auf sie erfordert 0 Maßnahmen, von denen es jedoch nur wenige gibt. Als nächstes folgen einige Kilobyte des Caches der ersten Ebene, auf den der Zugriff etwa 4 Taktzyklen dauert. Dann kommen ein paar hundert Kilobyte langsamerer Cache der zweiten Ebene. Dann ein paar Megabyte Cache in der dritten Ebene. Es ist viel langsamer, aber immer noch schneller als RAM. Als nächstes kommt ein relativ langsamer RAM.

    RAM kann als Cache für die lokale Festplatte betrachtet werden. Festplatten sind Arbeitspferde unter Speichergeräten. Sie sind groß, langsam und billig. Der Computer lädt Dateien von der Festplatte in den Arbeitsspeicher, wenn er daran arbeiten soll. Die Lücke in der Zugriffszeit zwischen RAM und Laufwerk ist enorm. Ein Laufwerk ist zehntausendmal langsamer als RAM und millionenfach langsamer als der Cache der ersten Ebene. Es ist rentabler, ein paar tausend Mal auf RAM zurückzugreifen als einmal auf eine Festplatte. Dieses Wissen basiert auf Datenstrukturen wie B-Bäumen , die versuchen, mehr Informationen in den Arbeitsspeicher zu stellen und um jeden Preis den Zugriff auf die Festplatte zu vermeiden.

    Die lokale Festplatte selbst kann als Cache für Daten auf Remoteservern betrachtet werden. Wenn Sie eine Website besuchen, speichert Ihr Browser die Bilder von der Webseite auf der Festplatte, sodass Sie sie beim erneuten Besuch nicht herunterladen müssen. Es gibt niedrigere Speicherhierarchien. Große Rechenzentren wie Google speichern große Datenmengen auf Band, die irgendwo in Lagern gespeichert sind und bei Bedarf manuell oder per Roboter verbunden werden müssen.

    Ein modernes System weist ungefähr die folgenden Eigenschaften auf:
    Cache-TypZugriffszeit (Ticks)Cache-Größe
    Register0Dutzende von Stücken
    L1-Cache432 KB
    L2-Cache10256 KB
    L3-Cache508 MB
    Rom2008 GB
    Festplattenpuffer100'00064 MB
    Lokale Festplatte10'000'0001000 GB
    Remote-Server1'000'000'000

    Schneller Speicher ist sehr teuer und langsamer ist sehr billig. Für Systemarchitekten ist es eine großartige Idee, die großen Größen des langsamen und billigen Speichers mit den kleinen Größen des schnellen und teuren Speichers zu kombinieren. Somit kann das System mit schnellen Speichergeschwindigkeiten laufen und hat langsame Kosten. Mal sehen, wie es funktioniert.

    Angenommen, Ihr Computer verfügt über 8 GB RAM und eine 1000-GB-Festplatte. Denken Sie jedoch, dass Sie nicht mit allen Daten auf der Festplatte gleichzeitig arbeiten. Sie laden das Betriebssystem, öffnen einen Webbrowser, einen Texteditor und einige andere Anwendungen und arbeiten mehrere Stunden mit ihnen. Alle diese Anwendungen befinden sich im RAM, sodass Ihr System nicht auf die Festplatte zugreifen muss. Dann schließen Sie natürlich eine Anwendung und öffnen eine andere, die Sie von der Festplatte in den Arbeitsspeicher laden müssen. Es dauert jedoch einige Sekunden. Danach arbeiten Sie mehrere Stunden mit dieser Anwendung, ohne auf die Festplatte zuzugreifen. Sie bemerken die langsame Festplatte nicht wirklich, da Sie in einem Moment nur mit einer kleinen Datenmenge arbeiten, die im RAM zwischengespeichert ist. Sie müssen nicht viel Geld für die Installation von 1024 GB RAM ausgeben, wodurch der Inhalt der gesamten Festplatte geladen werden kann. Wenn Sie dies tun würden, würden Sie kaum einen Unterschied in der Arbeit bemerken, es wäre "Geld den Bach runter".

    Dies ist auch bei kleinen Prozessor-Caches der Fall. Angenommen, Sie müssen Berechnungen für ein Array durchführen, das 1000 Elemente vom Typ int enthält . Ein solches Array belegt 4 KB und passt vollständig in den Cache der ersten Ebene von 32 KB. Das System versteht, dass Sie mit einem bestimmten RAM-Teil begonnen haben. Es kopiert dieses Teil in den Cache, und der Prozessor führt schnell Operationen an diesem Array aus und genießt die Geschwindigkeit des Caches. Dann wird das geänderte Array aus dem Cache zurück in den RAM kopiert. Eine Erhöhung der RAM-Geschwindigkeit auf die Cache-Geschwindigkeit würde keine spürbare Leistungssteigerung bewirken, aber die Systemkosten hunderte und tausende Male erhöhen. Dies alles gilt jedoch nur, wenn die Programme eine gute Lokalität haben.

    Lokalität


    Lokalität ist das Grundkonzept dieses Artikels. Programme mit guter Lokalität werden in der Regel schneller ausgeführt als Programme mit schlechter Lokalität. Es gibt zwei Arten von Lokalitäten. Wenn wir mehrmals auf denselben Speicherort zugreifen, ist dies ein vorübergehender Ort . Wenn wir auf die Daten zugreifen und dann auf andere Daten verweisen, die sich neben dem Original im Speicher befinden, ist dies eine räumliche Lokalität .

    Stellen Sie sich ein Programm vor, das die Elemente eines Arrays zusammenfasst:

    int sum(int size, int A[])
    {
        int i, sum = 0;
        for (i = 0; i < size; i++)
            sum += A[i];
        return sum;
    }
    

    In diesem Programm wird bei jeder Iteration der Schleife auf die Variablen sum und i zugegriffen . Sie haben eine gute zeitliche Lokalität und befinden sich in schnellen Prozessorregistern. Elemente von Array A haben eine schlechte zeitliche Lokalität, da wir nur einmal auf jedes Element zugreifen. Aber dann haben sie eine gute räumliche Lokalität - nachdem wir ein Element berührt haben, berühren wir die Elemente daneben. Alle Daten in diesem Programm haben entweder eine gute zeitliche Lokalität oder eine gute räumliche Lokalität, daher sagen wir, dass das Programm im Allgemeinen eine gute Lokalität hat.

    Wenn der Prozessor Daten aus dem Speicher liest, kopiert er sie in seinen Cache und löscht andere Daten aus dem Cache. Welche Daten er zum Löschen des Themas auswählt, ist kompliziert. Das Ergebnis ist jedoch, dass wenn Sie häufig auf einige Daten zugreifen, diese mit größerer Wahrscheinlichkeit im Cache verbleiben. Dies ist der Vorteil der zeitlichen Lokalität. Ein Programm ist besser dran, mit weniger Variablen zu arbeiten und häufiger darauf zuzugreifen.

    Das Verschieben von Daten zwischen Hierarchieebenen erfolgt in Blöcken einer bestimmten Größe. Beispielsweise verschiebt ein Core i7 Haswell-Prozessor Daten zwischen seinen Caches in Blöcken von 64 Byte. Betrachten Sie ein bestimmtes Beispiel. Wir führen das Programm auf einem Computer mit dem oben genannten Prozessor aus. Wir haben ein v- Array, das 8-Byte-Elemente vom Typ long enthält. Und wir durchlaufen die Elemente dieses Arrays nacheinander in einer Schleife. Wenn wir v [0] lesen , befindet es sich nicht im Cache, der Prozessor liest es in einem Block von 64 Bytes aus dem RAM in den Cache. Das heißt, die Elemente v [0] - v [7] werden an den Cache gesendet . Als nächstes gehen wir um die Elemente v [1] , v [2] , ..., v [7] herum . Alle von ihnen werden im Cache sein und wir werden schnell auf sie zugreifen können. Dann lesen wir das Element v [8] , das sich nicht im Cache befindet. Der Prozessor kopiert die Elemente ngr ; [8] - V [15] im Cache. Wir gehen schnell um diese Elemente herum, können aber das v- Element nicht im Cache finden [16] . Usw.

    Wenn Sie also einige Bytes aus dem Speicher lesen und dann die Bytes daneben lesen, befinden sie sich wahrscheinlich im Cache. Dies ist der Vorteil der räumlichen Lokalität. In jeder Phase der Berechnung müssen Sie sich bemühen, mit Daten zu arbeiten, die sich im Speicher in der Nähe befinden.

    Es ist ratsam, das Array nacheinander zu durchlaufen und seine Elemente einzeln zu lesen. Wenn Sie die Elemente der Matrix umgehen müssen, ist es besser, die Matrix zeilenweise und nicht in Spalten zu durchlaufen. Dies ergibt eine gute räumliche Lokalität. Jetzt können Sie verstehen, warum die Matrixsum2- Funktion langsamer war als die Matrixsum1- Funktion. Ein zweidimensionales Array befindet sich zeilenweise im Speicher: Zuerst befindet sich die erste Zeile, unmittelbar danach die zweite und so weiter. Die erste Funktion las die Matrixelemente Zeile für Zeile und bewegte sich nacheinander durch den Speicher, als würde sie ein großes eindimensionales Array umgehen. Diese Funktion liest hauptsächlich Daten aus dem Cache. Die zweite Funktion ging von Zeile zu Zeile und las jeweils ein Element. Sie schien von links nach rechts aus dem Gedächtnis zu springen, dann zum Anfang zurückzukehren und wieder von links nach rechts zu springen. Am Ende jeder Iteration wurde der Cache mit den letzten Zeilen verstopft, sodass der Cache zu Beginn der nächsten Iteration die ersten Zeilen nicht fand. Diese Funktion liest hauptsächlich Daten aus dem RAM.

    Cache-freundlicher Code


    Als Programmierer, sollten Sie versuchen , Code zu schreiben, sein soll in dem Cache - freundlich ( Cache freundlich ). In der Regel wird der Großteil der Berechnungen nur an wenigen Stellen im Programm durchgeführt. Normalerweise sind dies einige Schlüsselfunktionen und Zyklen. Wenn verschachtelte Schleifen vorhanden sind, sollte die Aufmerksamkeit auf die innersten Schleifen gerichtet werden, da der Code dort am häufigsten ausgeführt wird. Diese Programmorte müssen optimiert werden, um ihre Lokalität zu verbessern.

    Matrixberechnungen sind in Signalanalyseanwendungen und wissenschaftlichen Berechnungen sehr wichtig. Wenn die Aufgabe für Programmierer darin besteht, eine Matrixmultiplikationsfunktion zu schreiben, schreiben 99,9% von ihnen diese wie folgt:

    void matrixmult1(int size, double A[][size], double B[][size], double C[][size])
    {
        double sum;
        for (int i = 0; i < size; i++)
            for (int j = 0; j < size; j++) {
                sum = 0.0;
                for (int k = 0; k < size; k++)
                    sum += A[i][k]*B[k][j];
                C[i][j] = sum;
            }
    }
    

    Dieser Code wiederholt wörtlich die mathematische Definition der Matrixmultiplikation. Wir gehen alle Elemente der endgültigen Matrix Zeile für Zeile um und berechnen sie einzeln. Es gibt eine Ineffizienz im Code, dies ist der Ausdruck B [k] [j] in der innersten Schleife. Wir gehen um Matrix B in Spalten herum. Es scheint, dass nichts dagegen unternommen werden kann und sich abfinden muss. Aber es gibt einen Weg. Sie können dieselbe Berechnung unterschiedlich umschreiben:

    void matrixmult2(int size, double A[][size], double B[][size], double C[][size])
    {
        double r;
        for (int i = 0; i < size; i++)
            for (int k = 0; k < size; k++) {
                r = A[i][k];
                for (int j = 0; j < size; j++)
                    C[i][j] += r*B[k][j];
            }
    } 
    

    Jetzt sieht die Funktion sehr seltsam aus. Aber sie macht genau das Gleiche. Nur berechnen wir nicht jedes Element der endgültigen Matrix gleichzeitig, sondern scheinen die Elemente bei jeder Iteration teilweise zu berechnen. Die Schlüsseleigenschaft dieses Codes ist jedoch, dass wir in der inneren Schleife beide Matrizen zeilenweise durchlaufen. Auf einer Maschine mit einem Core i7 Haswell arbeitet die zweite Funktion bei großen Matrizen 12-mal schneller . Sie müssen ein wirklich weiser Programmierer sein, um Ihren Code auf diese Weise zu organisieren.

    Blockieren


    Es gibt eine Technik namens Blockieren . Angenommen, Sie müssen eine Berechnung für eine große Datenmenge durchführen, die nicht alle in einen Cache auf hoher Ebene passt. Sie teilen diese Daten in kleinere Blöcke auf, von denen jeder zwischengespeichert ist. Führen Sie die Berechnungen für diese Blöcke einzeln durch und kombinieren Sie dann das Ergebnis.

    Sie können dies anhand eines Beispiels demonstrieren. Angenommen, Sie haben einen gerichteten Graphen als Adjazenzmatrix dargestellt. Dies ist eine solche quadratische Matrix aus Nullen und Einsen. Wenn also das Matrixelement mit dem Index (i, j) gleich eins ist, gibt es eine Fläche vom Scheitelpunkt des Graphen i zum Scheitelpunkt j . Sie möchten dieses orientierte Diagramm in ein nicht orientiertes Diagramm umwandeln. Das heißt, wenn es eine Linie gibt(I, j) , sollte die gegenüberliegende Fläche erscheinen (j, i) . Beachten Sie, dass bei der Visualisierung der Matrix die Elemente (i, j) und (j, i) symmetrisch zur Diagonale sind. Diese Transformation ist einfach in Code zu implementieren:

    void convert1(int size, int G[][size])
    {
        for (int i = 0; i < size; i++)
            for (int j = 0; j < size; j++)
                G[i][j] = G[i][j] | G[j][i];
    }
    


    Das Blockieren erscheint natürlich. Stellen Sie sich eine große quadratische Matrix vor. Schneiden Sie diese Matrix nun mit horizontalen und vertikalen Linien aus, um sie beispielsweise in 16 gleiche Blöcke (vier Zeilen und vier Spalten) zu unterteilen. Wählen Sie zwei beliebige symmetrische Blöcke aus. Bitte beachten Sie, dass alle Elemente in einem Block ihre eigenen symmetrischen Elemente in einem anderen Block haben. Dies legt nahe, dass dieselbe Operation nacheinander für diese Blöcke ausgeführt werden kann. In diesem Fall arbeiten wir in jeder Phase mit nur zwei Blöcken. Wenn die Blöcke klein genug sind, passen sie in einen Cache auf hoher Ebene. Drücken Sie diese Idee im Code aus:

    void convert2(int size, int G[][size])
    {
        int block_size = size / 12;    // разбить на 12*12 блоков
                                       // представим, что делится без остатка
        for (int ii = 0; ii < size; ii += block_size) {
            for (int jj = 0; jj < size; jj += block_size) {
                int i_start = ii;    // индекс i для блока принимает значения [ii, ii + block_size)
                int i_end   = ii + block_size;
                int j_start = jj;    // индекс j для блока принимает значения [jj, jj + block_size)
                int j_end   = jj + block_size;
                // обходим блок
                for (int i = i_start; i < i_end; i++)
                    for (int j = j_start; j < j_end; j++)
                        G[i][j] = G[i][j] | G[j][i];
            }
        }
    }
    

    Es ist zu beachten, dass das Blockieren die Leistung auf Systemen mit leistungsstarken Prozessoren, die einen guten Prefetch durchführen, nicht verbessert. Auf Systemen, die nicht vorab abgerufen werden, kann das Blockieren die Leistung erheblich steigern.

    Auf einer Maschine mit einem Prozessor Core i7 Haswell zweite Funktion nicht schneller arbeiten. Auf einer Maschine mit einem einfacheren Pentium 2117U-Prozessor ist die zweite Funktion zweimal schneller . Auf Maschinen ohne Prefetch würde sich die Produktivität noch weiter verbessern.

    Welche Algorithmen sind schneller?


    Jeder weiß aus algorithmischen Kursen, dass Sie gute Algorithmen mit der geringsten Komplexität auswählen und schlechte vermeiden müssen .Algorithmen mit hoher Komplexität. Diese Schwierigkeiten bewerten jedoch die Ausführung des Algorithmus auf einer theoretischen Maschine, die durch unser Denken erzeugt wurde. Auf realen Maschinen kann ein theoretisch schlechter Algorithmus schneller ausgeführt werden als ein theoretisch guter. Denken Sie daran, dass das Abrufen von Daten aus dem RAM 200 Zyklen dauert und aus einem Cache der ersten Stufe 4 Zyklen 50-mal schneller sind. Wenn ein guter Algorithmus häufig den Speicher berührt und ein schlechter Algorithmus seine Daten in den Cache legt, kann ein guter Algorithmus langsamer als ein schlechter ausgeführt werden. Außerdem kann ein guter Algorithmus auf dem Prozessor schlechter laufen als ein schlechter. Ein guter Algorithmus macht beispielsweise Daten abhängig und kann die Prozessor-Pipeline nicht laden. Und ein schlechter Algorithmus wird dieses Problems beraubt und sendet bei jedem Zyklus einen neuen Befehl an die Pipeline. Mit anderen Worten, die Komplexität des Algorithmus ist nicht alles.

    Stellen Sie sich vor, Sie müssen eine Warteschlange mit Ganzzahlen implementieren, und an jeder Position in der Warteschlange können neue Elemente hinzugefügt werden. Sie wählen zwischen zwei Implementierungen: einem Array und einer verknüpften Liste. Um der Mitte des Arrays ein Element hinzuzufügen, müssen Sie in die rechte Hälfte des Arrays wechseln, was lineare Zeit in Anspruch nimmt. Um ein Element zur Mitte der Liste hinzuzufügen, müssen Sie die Liste zur Mitte durchgehen, was ebenfalls lineare Zeit in Anspruch nimmt. Sie denken, da sie die gleichen Schwierigkeiten haben, ist es besser, eine Liste auszuwählen. Darüber hinaus hat die Liste eine gute Eigenschaft. Die Liste kann ohne Einschränkung erweitert werden, und das Array muss erweitert werden, wenn es voll ist.

    Angenommen, wir haben auf beide Arten eine Warteschlange mit 1000 Elementen implementiert. Und wir müssen ein Element in die Mitte der Warteschlange einfügen. Listenelemente werden zufällig über den Speicher verteilt. Um 500 Elemente zu umgehen, benötigen wir 500 * 200 = 100'000 Kennzahlen. Das Array befindet sich nacheinander im Speicher, sodass wir die Geschwindigkeit des Caches der ersten Ebene genießen können. Mit mehreren Optimierungen können wir die Elemente des Arrays verschieben und 1-4 Zyklen pro Element verbringen. Wir werden die Hälfte des Arrays für maximal 500 * 4 = 2000 Taktzyklen verschieben. Das ist 50 mal schneller .

    Wenn im vorherigen Beispiel alle Ergänzungen am Anfang der Warteschlange stehen würden, wäre die Implementierung mit einer verknüpften Liste effizienter. Wenn sich ein Teil der Ergänzungen irgendwo in der Mitte der Warteschlange befindet, ist eine Array-Implementierung möglicherweise die bessere Wahl. Wir würden Taktiken für einige Operationen ausgeben und Taktiken für andere speichern. Und am Ende hätten sie gewinnen können.

    Fazit


    Das Speichersystem ist als Hierarchie von Speichergeräten mit kleinen und schnellen Geräten oben in der Hierarchie und großen und langsamen Geräten unten organisiert. Programme mit guter Lokalität arbeiten mit Daten aus Prozessor-Caches. Programme mit schlechter Lokalität arbeiten mit Daten aus relativ langsamem RAM.

    Programmierer, die die Natur der Speicherhierarchie verstehen, können ihre Programme so strukturieren, dass sich die Daten so hoch wie möglich in der Hierarchie befinden und der Prozessor sie schneller empfängt.

    Insbesondere werden folgende Techniken empfohlen:

    • Konzentrieren Sie sich auf innere Zyklen. Dort findet der größte Rechenaufwand und der größte Speicherzugriff statt.
    • Versuchen Sie, die räumliche Lokalität zu maximieren, indem Sie Objekte nacheinander aus dem Speicher in der Reihenfolge lesen, in der sie sich darin befinden.
    • Versuchen Sie, die zeitliche Lokalität zu maximieren, indem Sie Datenobjekte so oft wie möglich verwenden, nachdem sie aus dem Speicher gelesen wurden.

    Jetzt auch beliebt: