it-swarm.com.de

Warum sind elementweise Additionen in separaten Schleifen viel schneller als in einer kombinierten Schleife?

Angenommen, a1, b1, c1 und d1 zeigen auf Heap-Speicher, und mein numerischer Code hat die folgende Kernschleife.

const int n = 100000;

for (int j = 0; j < n; j++) {
    a1[j] += b1[j];
    c1[j] += d1[j];
}

Diese Schleife wird 10.000 Mal über eine andere äußere for Schleife ausgeführt. Um dies zu beschleunigen, habe ich den Code folgendermaßen geändert:

for (int j = 0; j < n; j++) {
    a1[j] += b1[j];
}

for (int j = 0; j < n; j++) {
    c1[j] += d1[j];
}

Kompiliert unter MS Visual C++ 10. mit vollständiger Optimierung und SSE2 aktiviert für 32-Bit auf einem Intel Core 2 Duo (x64), dem ersten Beispiel dauert 5,5 Sekunden und das Doppelschleifenbeispiel dauert nur 1,9 Sekunden. Meine Frage lautet: (Siehe meine umformulierte Frage unten.)

PS: Ich bin mir nicht sicher, ob dies hilft:

Das Zerlegen für die erste Schleife sieht im Prinzip so aus (dieser Block wird im gesamten Programm etwa fünfmal wiederholt):

movsd       xmm0,mmword ptr [edx+18h]
addsd       xmm0,mmword ptr [ecx+20h]
movsd       mmword ptr [ecx+20h],xmm0
movsd       xmm0,mmword ptr [esi+10h]
addsd       xmm0,mmword ptr [eax+30h]
movsd       mmword ptr [eax+30h],xmm0
movsd       xmm0,mmword ptr [edx+20h]
addsd       xmm0,mmword ptr [ecx+28h]
movsd       mmword ptr [ecx+28h],xmm0
movsd       xmm0,mmword ptr [esi+18h]
addsd       xmm0,mmword ptr [eax+38h]

Jede Schleife des Doppelschleifenbeispiels erzeugt diesen Code (der folgende Block wird ungefähr dreimal wiederholt):

addsd       xmm0,mmword ptr [eax+28h]
movsd       mmword ptr [eax+28h],xmm0
movsd       xmm0,mmword ptr [ecx+20h]
addsd       xmm0,mmword ptr [eax+30h]
movsd       mmword ptr [eax+30h],xmm0
movsd       xmm0,mmword ptr [ecx+28h]
addsd       xmm0,mmword ptr [eax+38h]
movsd       mmword ptr [eax+38h],xmm0
movsd       xmm0,mmword ptr [ecx+30h]
addsd       xmm0,mmword ptr [eax+40h]
movsd       mmword ptr [eax+40h],xmm0

Die Frage stellte sich als nicht relevant heraus, da das Verhalten stark von der Größe der Arrays (n) und dem CPU-Cache abhängt. Wenn es also weiteres Interesse gibt, formuliere ich die Frage neu:

Können Sie einen guten Einblick in die Details geben, die zu den unterschiedlichen Cache-Verhaltensweisen führen, wie sie in den fünf Regionen in der folgenden Grafik dargestellt sind?

Es könnte auch interessant sein, auf die Unterschiede zwischen CPU/Cache-Architekturen hinzuweisen, indem für diese CPUs ein ähnliches Diagramm bereitgestellt wird.

PPS: Hier ist der vollständige Code. Es verwendet TBBTick_Count für das Timing mit höherer Auflösung, das deaktiviert werden kann, indem das TBB_TIMING -Makro nicht definiert wird:

#include <iostream>
#include <iomanip>
#include <cmath>
#include <string>

//#define TBB_TIMING

#ifdef TBB_TIMING   
#include <tbb/tick_count.h>
using tbb::tick_count;
#else
#include <time.h>
#endif

using namespace std;

//#define preallocate_memory new_cont

enum { new_cont, new_sep };

double *a1, *b1, *c1, *d1;


void allo(int cont, int n)
{
    switch(cont) {
      case new_cont:
        a1 = new double[n*4];
        b1 = a1 + n;
        c1 = b1 + n;
        d1 = c1 + n;
        break;
      case new_sep:
        a1 = new double[n];
        b1 = new double[n];
        c1 = new double[n];
        d1 = new double[n];
        break;
    }

    for (int i = 0; i < n; i++) {
        a1[i] = 1.0;
        d1[i] = 1.0;
        c1[i] = 1.0;
        b1[i] = 1.0;
    }
}

void ff(int cont)
{
    switch(cont){
      case new_sep:
        delete[] b1;
        delete[] c1;
        delete[] d1;
      case new_cont:
        delete[] a1;
    }
}

double plain(int n, int m, int cont, int loops)
{
#ifndef preallocate_memory
    allo(cont,n);
#endif

#ifdef TBB_TIMING   
    tick_count t0 = tick_count::now();
#else
    clock_t start = clock();
#endif

    if (loops == 1) {
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++){
                a1[j] += b1[j];
                c1[j] += d1[j];
            }
        }
    } else {
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                a1[j] += b1[j];
            }
            for (int j = 0; j < n; j++) {
                c1[j] += d1[j];
            }
        }
    }
    double ret;

#ifdef TBB_TIMING   
    tick_count t1 = tick_count::now();
    ret = 2.0*double(n)*double(m)/(t1-t0).seconds();
#else
    clock_t end = clock();
    ret = 2.0*double(n)*double(m)/(double)(end - start) *double(CLOCKS_PER_SEC);
#endif

#ifndef preallocate_memory
    ff(cont);
#endif

    return ret;
}


void main()
{   
    freopen("C:\\test.csv", "w", stdout);

    char *s = " ";

    string na[2] ={"new_cont", "new_sep"};

    cout << "n";

    for (int j = 0; j < 2; j++)
        for (int i = 1; i <= 2; i++)
#ifdef preallocate_memory
            cout << s << i << "_loops_" << na[preallocate_memory];
#else
            cout << s << i << "_loops_" << na[j];
#endif

    cout << endl;

    long long nmax = 1000000;

#ifdef preallocate_memory
    allo(preallocate_memory, nmax);
#endif

    for (long long n = 1L; n < nmax; n = max(n+1, long long(n*1.2)))
    {
        const long long m = 10000000/n;
        cout << n;

        for (int j = 0; j < 2; j++)
            for (int i = 1; i <= 2; i++)
                cout << s << plain(n, m, j, i);
        cout << endl;
    }
}

(Es zeigt FLOP/s für verschiedene Werte von n.)

enter image description here

2145
Johannes Gerer

Nach weiterer Analyse ist dies meines Erachtens (zumindest teilweise) auf die Datenausrichtung der vier Zeiger zurückzuführen. Dies führt zu Konflikten zwischen Cache-Bänken und -Pfaden.

Wenn ich richtig geraten habe, wie Sie Ihre Arrays zuordnen, werden diese wahrscheinlich an der Seitenzeile ausgerichtet.

Dies bedeutet, dass alle Ihre Zugriffe in jeder Schleife auf den gleichen Cache-Weg fallen. Intel-Prozessoren verfügen jedoch seit einiger Zeit über eine 8-Wege-L1-Cache-Assoziativität. In Wirklichkeit ist die Leistung jedoch nicht ganz einheitlich. Der Zugriff auf 4-Wege ist immer noch langsamer als der Zugriff auf 2-Wege.

BEARBEITEN: Es sieht tatsächlich so aus, als würden Sie alle Arrays separat zuweisen. Normalerweise fordert der Allokator bei der Anforderung so großer Zuweisungen neue Seiten vom Betriebssystem an. Daher besteht eine hohe Wahrscheinlichkeit, dass große Zuordnungen mit demselben Versatz von einer Seitengrenze angezeigt werden.

Hier ist der Testcode:

int main(){
    const int n = 100000;

#ifdef ALLOCATE_SEPERATE
    double *a1 = (double*)malloc(n * sizeof(double));
    double *b1 = (double*)malloc(n * sizeof(double));
    double *c1 = (double*)malloc(n * sizeof(double));
    double *d1 = (double*)malloc(n * sizeof(double));
#else
    double *a1 = (double*)malloc(n * sizeof(double) * 4);
    double *b1 = a1 + n;
    double *c1 = b1 + n;
    double *d1 = c1 + n;
#endif

    //  Zero the data to prevent any chance of denormals.
    memset(a1,0,n * sizeof(double));
    memset(b1,0,n * sizeof(double));
    memset(c1,0,n * sizeof(double));
    memset(d1,0,n * sizeof(double));

    //  Print the addresses
    cout << a1 << endl;
    cout << b1 << endl;
    cout << c1 << endl;
    cout << d1 << endl;

    clock_t start = clock();

    int c = 0;
    while (c++ < 10000){

#if ONE_LOOP
        for(int j=0;j<n;j++){
            a1[j] += b1[j];
            c1[j] += d1[j];
        }
#else
        for(int j=0;j<n;j++){
            a1[j] += b1[j];
        }
        for(int j=0;j<n;j++){
            c1[j] += d1[j];
        }
#endif

    }

    clock_t end = clock();
    cout << "seconds = " << (double)(end - start) / CLOCKS_PER_SEC << endl;

    system("pause");
    return 0;
}

Benchmark-Ergebnisse:

BEARBEITEN: Ergebnisse auf einer aktuellen Core 2-Architekturmaschine:

2 x Intel Xeon X5482 Harpertown bei 3,2 GHz:

#define ALLOCATE_SEPERATE
#define ONE_LOOP
00600020
006D0020
007A0020
00870020
seconds = 6.206

#define ALLOCATE_SEPERATE
//#define ONE_LOOP
005E0020
006B0020
00780020
00850020
seconds = 2.116

//#define ALLOCATE_SEPERATE
#define ONE_LOOP
00570020
00633520
006F6A20
007B9F20
seconds = 1.894

//#define ALLOCATE_SEPERATE
//#define ONE_LOOP
008C0020
00983520
00A46A20
00B09F20
seconds = 1.993

Beobachtungen:

  • 6.206 Sekunden mit einer Schleife und 2.116 Sekunden mit zwei Schleifen. Dies gibt die Ergebnisse des OP exakt wieder.

  • In den ersten beiden Tests werden die Arrays separat zugewiesen. Sie werden feststellen, dass sie alle die gleiche Ausrichtung in Bezug auf die Seite haben.

  • In den zweiten beiden Tests werden die Arrays zusammengepackt, um diese Ausrichtung zu unterbrechen. Hier werden Sie feststellen, dass beide Schleifen schneller sind. Außerdem ist die zweite (Doppel-) Schleife jetzt die langsamere, wie Sie es normalerweise erwarten würden.

Wie @Stephen Cannon in den Kommentaren ausführt, besteht sehr wahrscheinlich die Möglichkeit, dass diese Ausrichtung falsches Aliasing in den Lade-/Speichereinheiten oder im Cache verursacht. Ich habe dafür gegoogelt und festgestellt, dass Intel tatsächlich einen Hardwarezähler für Partial Address Aliasing Stalls hat:

http://software.intel.com/sites/products/documentation/doclib/stdxe/2013/~amplifierxe/pmw_dp/events/partial_address_alias.html


5 Regionen - Erläuterungen

Region 1:

Dieser ist einfach. Der Datensatz ist so klein, dass die Leistung von Overhead wie Schleifen und Verzweigungen dominiert wird.

Region 2:

Hier nimmt mit zunehmender Datengröße der relative Overhead ab und die Leistung "sättigt" sich. Hier sind zwei Schleifen langsamer, weil sie doppelt so viel Schleifen- und Verzweigungsaufwand haben.

Ich bin mir nicht sicher, was genau hier vor sich geht ... Die Ausrichtung könnte immer noch einen Effekt haben, wenn Agner Fog erwähnt Cache-Bank-Konflikte . (Dieser Link handelt von Sandy Bridge, aber die Idee sollte immer noch für Core 2 gelten.)

Region 3:

Zu diesem Zeitpunkt passen die Daten nicht mehr in den L1-Cache. Die Leistung wird also durch die L1 <-> L2-Cache-Bandbreite begrenzt.

Region 4:

Der Leistungsabfall in der Einzelschleife ist das, was wir beobachten. Und wie bereits erwähnt, liegt dies an der Ausrichtung, die (höchstwahrscheinlich) falsches Aliasing in den Lade-/Speichereinheiten des Prozessors verursacht.

Damit jedoch ein falsches Aliasing auftritt, muss ein ausreichender Abstand zwischen den Datasets vorhanden sein. Aus diesem Grund wird dies in Region 3 nicht angezeigt.

Region 5:

Zu diesem Zeitpunkt passt nichts in den Cache. Sie sind also an die Speicherbandbreite gebunden.


2 x Intel X5482 Harpertown @ 3.2 GHzIntel Core i7 870 @ 2.8 GHzIntel Core i7 2600K @ 4.4 GHz

1638
Mysticial

OK, die richtige Antwort hat definitiv etwas mit dem CPU-Cache zu tun. Die Verwendung des Cache-Arguments kann jedoch sehr schwierig sein, insbesondere ohne Daten.

Es gibt viele Antworten, die zu vielen Diskussionen geführt haben, aber seien wir ehrlich: Cache-Probleme können sehr komplex und nicht eindimensional sein. Sie hängen stark von der Größe der Daten ab, daher war meine Frage unfair: Es stellte sich heraus, dass es sich um einen sehr interessanten Punkt im Cache-Diagramm handelt.

Die Antwort von @Mysticial überzeugte viele Leute (einschließlich mich), wahrscheinlich, weil es der einzige war, der sich auf Fakten zu verlassen schien, aber es war nur ein "Datenpunkt" der Wahrheit.

Deshalb habe ich seinen Test (mit einer fortlaufenden vs. getrennten Zuordnung) und den Rat von @James 'Answer kombiniert.

Die nachstehenden Grafiken zeigen, dass die meisten Antworten und insbesondere die meisten Kommentare zu den Fragen und Antworten je nach verwendetem Szenario und verwendeten Parametern als völlig falsch oder wahr eingestuft werden können.

Beachten Sie, dass meine erste Frage bei n = 100.0 war. Dieser Punkt weist (aus Versehen) ein besonderes Verhalten auf:

  1. Es weist die größte Diskrepanz zwischen der Version mit einer und zwei Schleifen auf (fast ein Faktor drei).

  2. Es ist der einzige Punkt, an dem Ein-Loop (nämlich mit kontinuierlicher Zuordnung) die Zwei-Loop-Version schlägt. (Dies ermöglichte Mystikals Antwort überhaupt.)

Das Ergebnis mit initialisierten Daten:

Enter image description here

Das Ergebnis unter Verwendung nicht initialisierter Daten (dies wurde von Mysticial getestet):

Enter image description here

Und das ist schwer zu erklären: Initialisierte Daten, die einmal vergeben und für jeden folgenden Testfall unterschiedlicher Vektorgröße wiederverwendet werden:

Enter image description here

Vorschlag

Jede leistungsbezogene Frage zu Stack Overflow auf niedriger Ebene sollte erforderlich sein, um MFLOPS-Informationen für den gesamten Bereich der Cache-relevanten Datengrößen bereitzustellen! Es ist eine Verschwendung von Zeit, über Antworten nachzudenken und diese ohne diese Informationen mit anderen zu diskutieren.

214
Johannes Gerer

Die zweite Schleife erfordert viel weniger Cache-Aktivität, sodass der Prozessor den Speicherbedarf leichter bewältigen kann.

76
Puppy

Stellen Sie sich vor, Sie arbeiten auf einem Computer, auf dem n genau der richtige Wert war, um nur zwei Ihrer Arrays gleichzeitig im Speicher zu halten halte alle vier.

Unter der Annahme einer einfachen Caching-Richtlinie LIFO lautet dieser Code:

for(int j=0;j<n;j++){
    a[j] += b[j];
}
for(int j=0;j<n;j++){
    c[j] += d[j];
}

würde zuerst bewirken, dass a und b in RAM geladen und dann vollständig im RAM bearbeitet werden. Wenn die zweite Schleife startet, werden c und d von der Festplatte in RAM geladen und bearbeitet.

die andere Schleife

for(int j=0;j<n;j++){
    a[j] += b[j];
    c[j] += d[j];
}

wird zwei Arrays auslagern und in den anderen zwei blättern jedes Mal um die Schleife. Dies wäre offensichtlich viel langsamer.

Wahrscheinlich sehen Sie in Ihren Tests kein Platten-Caching, aber Sie sehen wahrscheinlich die Nebenwirkungen einer anderen Form des Caching.


Es scheint hier ein wenig Verwirrung/Missverständnis zu geben, deshalb werde ich versuchen, ein wenig anhand eines Beispiels zu erläutern.

Sagen Sie n = 2 und wir arbeiten mit Bytes. In meinem Szenario haben wir also nur 4 Bytes RAM und der Rest unseres Speichers ist bedeutend langsamer (sagen wir 100-mal längerer Zugriff).

Unter der Annahme einer ziemlich dummen Caching-Richtlinie von , wenn sich das Byte nicht im Cache befindet, platzieren Sie es dort und holen Sie sich auch das folgende Byte, während wir dabei sind Holen Sie sich ein Szenario wie folgt:

  • Mit

    for(int j=0;j<n;j++){
     a[j] += b[j];
    }
    for(int j=0;j<n;j++){
     c[j] += d[j];
    }
    
  • cache a[0] und a[1] dann b[0] und b[1] und setze a[0] = a[0] + b[0] in den Cache - es sind jetzt vier Bytes im Cache, a[0], a[1] und b[0], b[1]. Kosten = 100 + 100.

  • setze a[1] = a[1] + b[1] in den Cache. Kosten = 1 + 1.
  • Wiederholen Sie diesen Vorgang für c und d.
  • Gesamtkosten = (100 + 100 + 1 + 1) * 2 = 404

  • Mit

    for(int j=0;j<n;j++){
     a[j] += b[j];
     c[j] += d[j];
    }
    
  • cache a[0] und a[1] dann b[0] und b[1] und setze a[0] = a[0] + b[0] in den Cache - es sind jetzt vier Bytes im Cache, a[0], a[1] und b[0], b[1]. Kosten = 100 + 100.

  • werfen Sie a[0], a[1], b[0], b[1] aus dem Cache und Cache c[0] und c[1] dann d[0] und d[1] und setzen Sie c[0] = c[0] + d[0] in den Cache. Kosten = 100 + 100.
  • Ich vermute, Sie beginnen zu sehen, wohin ich gehe.
  • Gesamtkosten = (100 + 100 + 100 + 100) * 2 = 800

Dies ist ein klassisches Cache-Thrash-Szenario.

45
OldCurmudgeon

Dies liegt nicht an einem anderen Code, sondern an der Zwischenspeicherung: RAM ist langsamer als die CPU-Register und es befindet sich ein Cache-Speicher in der CPU, um zu vermeiden, dass jedes Mal eine Variable in RAM geschrieben wird verändert sich. Der Cache ist jedoch nicht so groß wie der RAM, sodass er nur einen Bruchteil davon abbildet.

Der erste Code ändert entfernte Speicheradressen, indem er sie bei jeder Schleife abwechselt, so dass der Cache kontinuierlich ungültig gemacht werden muss.

Der zweite Code wechselt nicht: Er fließt nur zweimal über benachbarte Adressen. Dadurch wird der gesamte Auftrag im Cache abgeschlossen und erst nach dem Start der zweiten Schleife ungültig.

30

Ich kann die hier diskutierten Ergebnisse nicht replizieren.

Ich weiß nicht, ob ein schlechter Benchmark-Code daran schuld ist oder nicht, aber die beiden Methoden liegen auf meinem Computer mit dem folgenden Code innerhalb von 10% voneinander, und eine Schleife ist normalerweise nur geringfügig schneller als zwei - wie Sie möchten erwarten von.

Die Arraygrößen reichten von 2 ^ 16 bis 2 ^ 24, wobei acht Schleifen verwendet wurden. Ich habe darauf geachtet, die Quell-Arrays zu initialisieren, damit die += -Zuweisung das FPU nicht aufforderte, Speichermüll hinzuzufügen, der als Double interpretiert wurde.

Ich habe mit verschiedenen Schemata herumgespielt, wie z. B. die Zuordnung von b[j], d[j] zu InitToZero[j] in den Schleifen und auch die Verwendung von += b[j] = 1 und += d[j] = 1, und ich habe ziemlich konsistente Ergebnisse.

Das Initialisieren von b und d innerhalb der Schleife mit InitToZero[j] brachte erwartungsgemäß den Vorteil des kombinierten Ansatzes, da diese vor den Zuweisungen zu a und c, aber immer noch innerhalb von 10% durchgeführt wurden. Stelle dir das vor.

Hardware ist Dell XPS 85 mit Generation 3 Core i7 bei 3,4 GHz und 8 GB Speicher. Für 2 ^ 16 bis 2 ^ 24 unter Verwendung von acht Schleifen betrug die kumulative Zeit 44,987 bzw. 40,965. Visual C++ 2010, vollständig optimiert.

PS: Ich habe die Schleifen so geändert, dass sie bis auf Null herunter zählen, und die kombinierte Methode war geringfügig schneller. Ich kratzte mich am Kopf. Beachten Sie die neue Größe des Arrays und die Anzahl der Schleifen.

// MemBufferMystery.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <iostream>
#include <cmath>
#include <string>
#include <time.h>

#define  dbl    double
#define  MAX_ARRAY_SZ    262145    //16777216    // AKA (2^24)
#define  STEP_SZ           1024    //   65536    // AKA (2^16)

int _tmain(int argc, _TCHAR* argv[]) {
    long i, j, ArraySz = 0,  LoopKnt = 1024;
    time_t start, Cumulative_Combined = 0, Cumulative_Separate = 0;
    dbl *a = NULL, *b = NULL, *c = NULL, *d = NULL, *InitToOnes = NULL;

    a = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    b = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    c = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    d = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    InitToOnes = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    // Initialize array to 1.0 second.
    for(j = 0; j< MAX_ARRAY_SZ; j++) {
        InitToOnes[j] = 1.0;
    }

    // Increase size of arrays and time
    for(ArraySz = STEP_SZ; ArraySz<MAX_ARRAY_SZ; ArraySz += STEP_SZ) {
        a = (dbl *)realloc(a, ArraySz * sizeof(dbl));
        b = (dbl *)realloc(b, ArraySz * sizeof(dbl));
        c = (dbl *)realloc(c, ArraySz * sizeof(dbl));
        d = (dbl *)realloc(d, ArraySz * sizeof(dbl));
        // Outside the timing loop, initialize
        // b and d arrays to 1.0 sec for consistent += performance.
        memcpy((void *)b, (void *)InitToOnes, ArraySz * sizeof(dbl));
        memcpy((void *)d, (void *)InitToOnes, ArraySz * sizeof(dbl));

        start = clock();
        for(i = LoopKnt; i; i--) {
            for(j = ArraySz; j; j--) {
                a[j] += b[j];
                c[j] += d[j];
            }
        }
        Cumulative_Combined += (clock()-start);
        printf("\n %6i miliseconds for combined array sizes %i and %i loops",
                (int)(clock()-start), ArraySz, LoopKnt);
        start = clock();
        for(i = LoopKnt; i; i--) {
            for(j = ArraySz; j; j--) {
                a[j] += b[j];
            }
            for(j = ArraySz; j; j--) {
                c[j] += d[j];
            }
        }
        Cumulative_Separate += (clock()-start);
        printf("\n %6i miliseconds for separate array sizes %i and %i loops \n",
                (int)(clock()-start), ArraySz, LoopKnt);
    }
    printf("\n Cumulative combined array processing took %10.3f seconds",
            (dbl)(Cumulative_Combined/(dbl)CLOCKS_PER_SEC));
    printf("\n Cumulative seperate array processing took %10.3f seconds",
        (dbl)(Cumulative_Separate/(dbl)CLOCKS_PER_SEC));
    getchar();

    free(a); free(b); free(c); free(d); free(InitToOnes);
    return 0;
}

Ich bin mir nicht sicher, warum entschieden wurde, dass MFLOPS eine relevante Metrik ist. Ich dachte, die Idee sei, mich auf Speicherzugriffe zu konzentrieren, also habe ich versucht, die Gleitkomma-Rechenzeit zu minimieren. Ich bin im += abgereist, bin mir aber nicht sicher warum.

Eine direkte Zuweisung ohne Berechnung wäre ein sauberer Test der Speicherzugriffszeit und würde einen Test erzeugen, der unabhängig von der Schleifenzahl einheitlich ist. Vielleicht habe ich etwas im Gespräch verpasst, aber es lohnt sich, zweimal darüber nachzudenken. Wird das Plus aus der Zuordnung herausgerechnet, ist die kumulierte Zeit mit jeweils 31 Sekunden nahezu identisch.

19
user1899861

Dies liegt daran, dass die CPU nicht so viele Cache-Fehler aufweist (und warten muss, bis die Array-Daten von den RAM Chips stammen). Es wäre interessant für Sie, die Größe der Arrays kontinuierlich anzupassen, damit Sie die Größen des Level 1-Cache (L1) und dann des Level 2-Cache (überschreiten. L2) Ihrer CPU und zeichnen Sie die Ausführungszeit Ihres Codes in Abhängigkeit von der Größe der Arrays auf. Das Diagramm sollte keine gerade Linie sein, wie Sie es erwarten würden.

16
James

Die erste Schleife schreibt abwechselnd in jede Variable. Die zweiten und dritten machen nur kleine Sprünge von Elementgröße.

Versuchen Sie, zwei parallele Linien von 20 Kreuzen mit einem Stift und Papier in einem Abstand von 20 cm zu schreiben. Versuchen Sie einmal, die eine und dann die andere Zeile zu beenden, und versuchen Sie es ein anderes Mal, indem Sie abwechselnd ein Kreuz in jede Zeile schreiben.

13
Guillaume Kiz

Es kann altes C++ und Optimierungen sein. Auf meinem Computer habe ich fast die gleiche Geschwindigkeit erhalten:

Eine Schleife: 1,577 ms

Zwei Schleifen: 1,507 ms

Ich führe Visual Studio 2015 auf einem E5-1620 3,5-GHz-Prozessor mit 16 GB RAM aus.

1
mathengineer