Java 8-mal schneller mit Arrays als std :: vector in C ++. Was habe ich falsch gemacht?

88

Ich habe den folgenden Java-Code mit mehreren großen Arrays, deren Größe sich nie ändert. Es läuft in 1100 ms auf meinem Computer.

Ich habe den gleichen Code in C ++ implementiert und verwendet std::vector.

Die Zeit der C ++ - Implementierung, in der genau derselbe Code ausgeführt wird, beträgt auf meinem Computer 8800 ms. Was habe ich falsch gemacht, damit es so langsam läuft?

Grundsätzlich macht der Code Folgendes:

for (int i = 0; i < numberOfCells; ++i) {
        h[i] =  h[i] + 1;
        floodedCells[i] =  !floodedCells[i];
        floodedCellsTimeInterval[i] =  !floodedCellsTimeInterval[i];
        qInflow[i] =  qInflow[i] + 1;
}

Es durchläuft verschiedene Arrays mit einer Größe von ca. 20000.

Sie finden beide Implementierungen unter den folgenden Links:

(Auf ideone konnte ich die Schleife aufgrund der zeitlichen Begrenzung nur 400-mal statt 2000-mal ausführen. Aber auch hier gibt es einen dreifachen Unterschied.)

RobinXSI
quelle
42
std::vector<bool>verwendet ein Bit pro Element, um Platz zu sparen, was zu einer starken Bitverschiebung führt. Wenn Sie Geschwindigkeit wollen, sollten Sie sich davon fernhalten. Verwenden Sie std::vector<int>stattdessen.
Molbdnilo
44
@molbdnilo Oder std :: vector <char>. Es ist nicht nötig , so viel zu verschwenden ;-)
stefan
7
Lustig genug. Die c ++ - Version ist schneller, wenn die Anzahl der Zellen 200 beträgt. Cache-Lokalität?
Kapitän Giraffe
9
Teil II: Sie sind viel besser dran, eine separate Klasse / Struktur zu erstellen, die eines von jedem Mitglied der Arrays enthält, und dann ein einzelnes Array von Objekten dieser Struktur zu haben, da Sie dann tatsächlich nur einmal durch den Speicher iterieren eine Richtung.
Timo Geusch
9
@ TimoGeusch: Obwohl ich denke h[i] += 1;oder (noch besser) ++h[i]besser lesbar ist als h[i] = h[i] + 1;, wäre ich etwas überrascht, einen signifikanten Geschwindigkeitsunterschied zwischen ihnen zu sehen. Ein Compiler kann "herausfinden", dass beide dasselbe tun und in beiden Fällen denselben Code generieren (zumindest in den meisten Fällen).
Jerry Coffin

Antworten:

36

Hier ist die C ++ - Version mit den in einer Struktur gesammelten Daten pro Knoten und einem einzelnen verwendeten Vektor dieser Struktur:

#include <vector>
#include <cmath>
#include <iostream>



class FloodIsolation {
public:
  FloodIsolation() :
      numberOfCells(20000),
      data(numberOfCells)
  {
  }
  ~FloodIsolation(){
  }

  void isUpdateNeeded() {
    for (int i = 0; i < numberOfCells; ++i) {
       data[i].h = data[i].h + 1;
       data[i].floodedCells = !data[i].floodedCells;
       data[i].floodedCellsTimeInterval = !data[i].floodedCellsTimeInterval;
       data[i].qInflow = data[i].qInflow + 1;
       data[i].qStartTime = data[i].qStartTime + 1;
       data[i].qEndTime = data[i].qEndTime + 1;
       data[i].lowerFloorCells = data[i].lowerFloorCells + 1;
       data[i].cellLocationX = data[i].cellLocationX + 1;
       data[i].cellLocationY = data[i].cellLocationY + 1;
       data[i].cellLocationZ = data[i].cellLocationZ + 1;
       data[i].levelOfCell = data[i].levelOfCell + 1;
       data[i].valueOfCellIds = data[i].valueOfCellIds + 1;
       data[i].h0 = data[i].h0 + 1;
       data[i].vU = data[i].vU + 1;
       data[i].vV = data[i].vV + 1;
       data[i].vUh = data[i].vUh + 1;
       data[i].vVh = data[i].vVh + 1;
       data[i].vUh0 = data[i].vUh0 + 1;
       data[i].vVh0 = data[i].vVh0 + 1;
       data[i].ghh = data[i].ghh + 1;
       data[i].sfx = data[i].sfx + 1;
       data[i].sfy = data[i].sfy + 1;
       data[i].qIn = data[i].qIn + 1;


      for(int j = 0; j < nEdges; ++j) {
        data[i].flagInterface[j] = !data[i].flagInterface[j];
        data[i].typeInterface[j] = data[i].typeInterface[j] + 1;
        data[i].neighborIds[j] = data[i].neighborIds[j] + 1;
      }
    }

  }

private:

  const int numberOfCells;
  static const int nEdges = 6;
  struct data_t {
    bool floodedCells = 0;
    bool floodedCellsTimeInterval = 0;

    double valueOfCellIds = 0;
    double h = 0;

    double h0 = 0;
    double vU = 0;
    double vV = 0;
    double vUh = 0;
    double vVh = 0;
    double vUh0 = 0;
    double vVh0 = 0;
    double ghh = 0;
    double sfx = 0;
    double sfy = 0;
    double qInflow = 0;
    double qStartTime = 0;
    double qEndTime = 0;
    double qIn = 0;
    double nx = 0;
    double ny = 0;
    double floorLevels = 0;
    int lowerFloorCells = 0;
    bool floorCompleteleyFilled = 0;
    double cellLocationX = 0;
    double cellLocationY = 0;
    double cellLocationZ = 0;
    int levelOfCell = 0;
    bool flagInterface[nEdges] = {};
    int typeInterface[nEdges] = {};
    int neighborIds[nEdges] = {};
  };
  std::vector<data_t> data;

};

int main() {
  std::ios_base::sync_with_stdio(false);
  FloodIsolation isolation;
  clock_t start = clock();
  for (int i = 0; i < 400; ++i) {
    if(i % 100 == 0) {
      std::cout << i << "\n";
    }
    isolation.isUpdateNeeded();
  }
  clock_t stop = clock();
  std::cout << "Time: " << difftime(stop, start) / 1000 << "\n";
}

Live-Beispiel

Die Zeit ist jetzt 2x so schnell wie die Java-Version. (846 gegen 1631).

Wahrscheinlich hat die JIT das Brennen des Cache beim Zugriff auf Daten überall bemerkt und Ihren Code in eine logisch ähnliche, aber effizientere Reihenfolge umgewandelt.

Ich habe auch die stdio-Synchronisation deaktiviert, da dies nur benötigt wird, wenn Sie printf/ scanfmit C ++ std::coutund mischen std::cin. Zwar drucken Sie nur wenige Werte aus, aber das Standardverhalten von C ++ beim Drucken ist zu paranoid und ineffizient.

Wenn nEdgeses sich nicht um einen tatsächlichen konstanten Wert handelt, müssen die 3 "Array" -Werte aus dem entfernt werden struct. Das sollte keinen großen Leistungseinbruch verursachen.

Möglicherweise können Sie eine weitere Leistungssteigerung erzielen, indem Sie die Werte sortieren, structindem Sie die Größe verringern und so den Speicherbedarf verringern (und den Zugriff auch sortieren, wenn dies keine Rolle spielt). Aber ich bin mir nicht sicher.

Als Faustregel gilt, dass ein einzelner Cache-Fehler 100-mal teurer ist als eine Anweisung. Das Anordnen Ihrer Daten zur Cache-Kohärenz hat viel Wert.

Wenn eine Neuanordnung der Daten in a nicht möglich structist, können Sie Ihre Iteration so ändern, dass sie sich nacheinander über jedem Container befindet.

Beachten Sie außerdem, dass die Java- und C ++ - Versionen einige geringfügige Unterschiede aufwiesen. Die eine, die ich entdeckte, war, dass die Java-Version 3 Variablen in der "für jede Kante" -Schleife hat, während die C ++ - nur 2 hatte. Ich habe meine mit Java übereinstimmen lassen. Ich weiß nicht, ob es noch andere gibt.

Yakk - Adam Nevraumont
quelle
44

Ja, der Cache in der C ++ - Version muss gehämmert werden. Es scheint, dass die JIT besser dafür gerüstet ist.

Wenn Sie das Äußere forin isUpdateNeeded () in kürzere Snippets ändern . Der Unterschied verschwindet.

Das folgende Beispiel führt zu einer 4-fachen Beschleunigung.

void isUpdateNeeded() {
    for (int i = 0; i < numberOfCells; ++i) {
        h[i] =  h[i] + 1;
        floodedCells[i] =  !floodedCells[i];
        floodedCellsTimeInterval[i] =  !floodedCellsTimeInterval[i];
        qInflow[i] =  qInflow[i] + 1;
        qStartTime[i] =  qStartTime[i] + 1;
        qEndTime[i] =  qEndTime[i] + 1;
    }

    for (int i = 0; i < numberOfCells; ++i) {
        lowerFloorCells[i] =  lowerFloorCells[i] + 1;
        cellLocationX[i] =  cellLocationX[i] + 1;
        cellLocationY[i] =  cellLocationY[i] + 1;
        cellLocationZ[i] =  cellLocationZ[i] + 1;
        levelOfCell[i] =  levelOfCell[i] + 1;
        valueOfCellIds[i] =  valueOfCellIds[i] + 1;
        h0[i] =  h0[i] + 1;
        vU[i] =  vU[i] + 1;
        vV[i] =  vV[i] + 1;
        vUh[i] =  vUh[i] + 1;
        vVh[i] =  vVh[i] + 1;
    }
    for (int i = 0; i < numberOfCells; ++i) {
        vUh0[i] =  vUh0[i] + 1;
        vVh0[i] =  vVh0[i] + 1;
        ghh[i] =  ghh[i] + 1;
        sfx[i] =  sfx[i] + 1;
        sfy[i] =  sfy[i] + 1;
        qIn[i] =  qIn[i] + 1;
        for(int j = 0; j < nEdges; ++j) {
            neighborIds[i * nEdges + j] = neighborIds[i * nEdges + j] + 1;
        }
        for(int j = 0; j < nEdges; ++j) {
            typeInterface[i * nEdges + j] = typeInterface[i * nEdges + j] + 1;
        }
    }

}

Dies zeigt in angemessenem Maße, dass Cache-Fehler der Grund für die Verlangsamung sind. Es ist auch wichtig zu beachten, dass die Variablen nicht abhängig sind, sodass leicht eine Thread-Lösung erstellt werden kann.

Ordnung wiederhergestellt

Gemäß dem Kommentar von stefans habe ich versucht, sie in einer Struktur unter Verwendung der Originalgrößen zu gruppieren. Dies entfernt den unmittelbaren Cache-Druck auf ähnliche Weise. Das Ergebnis ist, dass die c ++ (CCFLAG -O3) -Version etwa 15% schneller ist als die Java-Version.

Varning weder kurz noch hübsch.

#include <vector>
#include <cmath>
#include <iostream>
 
 
 
class FloodIsolation {
    struct item{
      char floodedCells;
      char floodedCellsTimeInterval;
      double valueOfCellIds;
      double h;
      double h0;
      double vU;
      double vV;
      double vUh;
      double vVh;
      double vUh0;
      double vVh0;
      double sfx;
      double sfy;
      double qInflow;
      double qStartTime;
      double qEndTime;
      double qIn;
      double nx;
      double ny;
      double ghh;
      double floorLevels;
      int lowerFloorCells;
      char flagInterface;
      char floorCompletelyFilled;
      double cellLocationX;
      double cellLocationY;
      double cellLocationZ;
      int levelOfCell;
    };
    struct inner_item{
      int typeInterface;
      int neighborIds;
    };

    std::vector<inner_item> inner_data;
    std::vector<item> data;

public:
    FloodIsolation() :
            numberOfCells(20000), inner_data(numberOfCells * nEdges), data(numberOfCells)
   {

    }
    ~FloodIsolation(){
    }
 
    void isUpdateNeeded() {
        for (int i = 0; i < numberOfCells; ++i) {
            data[i].h = data[i].h + 1;
            data[i].floodedCells = !data[i].floodedCells;
            data[i].floodedCellsTimeInterval = !data[i].floodedCellsTimeInterval;
            data[i].qInflow = data[i].qInflow + 1;
            data[i].qStartTime = data[i].qStartTime + 1;
            data[i].qEndTime = data[i].qEndTime + 1;
            data[i].lowerFloorCells = data[i].lowerFloorCells + 1;
            data[i].cellLocationX = data[i].cellLocationX + 1;
            data[i].cellLocationY = data[i].cellLocationY + 1;
            data[i].cellLocationZ = data[i].cellLocationZ + 1;
            data[i].levelOfCell = data[i].levelOfCell + 1;
            data[i].valueOfCellIds = data[i].valueOfCellIds + 1;
            data[i].h0 = data[i].h0 + 1;
            data[i].vU = data[i].vU + 1;
            data[i].vV = data[i].vV + 1;
            data[i].vUh = data[i].vUh + 1;
            data[i].vVh = data[i].vVh + 1;
            data[i].vUh0 = data[i].vUh0 + 1;
            data[i].vVh0 = data[i].vVh0 + 1;
            data[i].ghh = data[i].ghh + 1;
            data[i].sfx = data[i].sfx + 1;
            data[i].sfy = data[i].sfy + 1;
            data[i].qIn = data[i].qIn + 1;
            for(int j = 0; j < nEdges; ++j) {
                inner_data[i * nEdges + j].neighborIds = inner_data[i * nEdges + j].neighborIds + 1;
                inner_data[i * nEdges + j].typeInterface = inner_data[i * nEdges + j].typeInterface + 1;
            }
        }
 
    }
 
    static const int nEdges;
private:
 
    const int numberOfCells;

};
 
const int FloodIsolation::nEdges = 6;

int main() {
    FloodIsolation isolation;
    clock_t start = clock();
    for (int i = 0; i < 4400; ++i) {
        if(i % 100 == 0) {
            std::cout << i << "\n";
        }
        isolation.isUpdateNeeded();
    }

    clock_t stop = clock();
    std::cout << "Time: " << difftime(stop, start) / 1000 << "\n";
}
                                                                              

Mein Ergebnis unterscheidet sich geringfügig von Jerry Coffins für die Originalgrößen. Für mich bleiben die Unterschiede. Es könnte durchaus meine Java-Version 1.7.0_75 sein.

Kapitän Giraffe
quelle
12
Es könnte eine gute Idee sein, diese Daten in einer Struktur zu gruppieren und nur einen Vektor zu haben
stefan
Nun, ich bin auf dem Handy, also kann ich keine Messungen durchführen ;-), aber der eine Vektor sollte gut sein (auch in Bezug auf die Zuordnungen)
stefan
1
Hilft die Verwendung ++in irgendeiner Form? x = x + 1scheint schrecklich klobig im Vergleich zu ++x.
Tadman
3
Bitte korrigieren Sie das falsch geschriebene Wort "Ergebnis". Es
bringt
1
Wenn der gesamte Iterator in ein einzelnes Register passt, ist das Erstellen einer Kopie in einigen Fällen möglicherweise sogar schneller als das Aktualisieren an Ort und Stelle. Wenn Sie ein Update an Ort und Stelle durchführen, liegt dies daran, dass Sie den aktualisierten Wert sehr wahrscheinlich unmittelbar danach verwenden. Sie haben also eine Lese-nach-Schreib-Abhängigkeit. Wenn Sie aktualisieren, aber nur den alten Wert benötigen, hängen diese Vorgänge nicht voneinander ab, und die CPU hat mehr Platz, um sie parallel auszuführen, z. B. in verschiedenen Pipelines, wodurch der effektive IPC erhöht wird.
Piotr Kołaczkowski
20

Wie @Stefan in einem Kommentar zur Antwort von @ CaptainGiraffe vermutet hat, gewinnen Sie einiges, wenn Sie einen Vektor von Strukturen anstelle einer Struktur von Vektoren verwenden. Der korrigierte Code sieht folgendermaßen aus:

#include <vector>
#include <cmath>
#include <iostream>
#include <time.h>

class FloodIsolation {
public:
    FloodIsolation() :
            h(0),
            floodedCells(0),
            floodedCellsTimeInterval(0),
            qInflow(0),
            qStartTime(0),
            qEndTime(0),
            lowerFloorCells(0),
            cellLocationX(0),
            cellLocationY(0),
            cellLocationZ(0),
            levelOfCell(0),
            valueOfCellIds(0),
            h0(0),
            vU(0),
            vV(0),
            vUh(0),
            vVh(0),
            vUh0(0),
            vVh0(0),
            ghh(0),
            sfx(0),
            sfy(0),
            qIn(0),
            typeInterface(nEdges, 0),
            neighborIds(nEdges, 0)
    {
    }

    ~FloodIsolation(){
    }

    void Update() {
        h =  h + 1;
        floodedCells =  !floodedCells;
        floodedCellsTimeInterval =  !floodedCellsTimeInterval;
        qInflow =  qInflow + 1;
        qStartTime =  qStartTime + 1;
        qEndTime =  qEndTime + 1;
        lowerFloorCells =  lowerFloorCells + 1;
        cellLocationX =  cellLocationX + 1;
        cellLocationY =  cellLocationY + 1;
        cellLocationZ =  cellLocationZ + 1;
        levelOfCell =  levelOfCell + 1;
        valueOfCellIds =  valueOfCellIds + 1;
        h0 =  h0 + 1;
        vU =  vU + 1;
        vV =  vV + 1;
        vUh =  vUh + 1;
        vVh =  vVh + 1;
        vUh0 =  vUh0 + 1;
        vVh0 =  vVh0 + 1;
        ghh =  ghh + 1;
        sfx =  sfx + 1;
        sfy =  sfy + 1;
        qIn =  qIn + 1;
        for(int j = 0; j < nEdges; ++j) {
            ++typeInterface[j];
            ++neighborIds[j];
        }       
    }

private:

    static const int nEdges = 6;
    bool floodedCells;
    bool floodedCellsTimeInterval;

    std::vector<int> neighborIds;
    double valueOfCellIds;
    double h;
    double h0;
    double vU;
    double vV;
    double vUh;
    double vVh;
    double vUh0;
    double vVh0;
    double ghh;
    double sfx;
    double sfy;
    double qInflow;
    double qStartTime;
    double qEndTime;
    double qIn;
    double nx;
    double ny;
    double floorLevels;
    int lowerFloorCells;
    bool flagInterface;
    std::vector<int> typeInterface;
    bool floorCompleteleyFilled;
    double cellLocationX;
    double cellLocationY;
    double cellLocationZ;
    int levelOfCell;
};

int main() {
    std::vector<FloodIsolation> isolation(20000);
    clock_t start = clock();
    for (int i = 0; i < 400; ++i) {
        if(i % 100 == 0) {
            std::cout << i << "\n";
        }

        for (auto &f : isolation)
            f.Update();
    }
    clock_t stop = clock();
    std::cout << "Time: " << difftime(stop, start) / 1000 << "\n";
}

Mit dem Compiler aus VC ++ 2015 CTP kompiliert -EHsc -O2b2 -GL -Qpar, erhalte ich folgende Ergebnisse:

0
100
200
300
Time: 0.135

Das Kompilieren mit g ++ führt zu einem etwas langsameren Ergebnis:

0
100
200
300
Time: 0.156

Auf derselben Hardware erhalte ich mit dem Compiler / JVM von Java 8u45 folgende Ergebnisse:

0
100
200
300
Time: 181

Dies ist ungefähr 35% langsamer als die Version von VC ++ und ungefähr 16% langsamer als die Version von g ++.

Wenn wir die Anzahl der Iterationen auf das gewünschte Jahr 2000 erhöhen, sinkt die Differenz auf nur 3%, was darauf hindeutet, dass ein Teil des Vorteils von C ++ in diesem Fall einfach ein schnelleres Laden ist (ein beständiges Problem mit Java), das nicht wirklich in der Ausführung selbst liegt. Dies überrascht mich in diesem Fall nicht - die gemessene Berechnung (im veröffentlichten Code) ist so trivial, dass ich bezweifle, dass die meisten Compiler eine ganze Menge tun können, um sie zu optimieren.

Jerry Sarg
quelle
1
Es gibt noch Verbesserungspotenzial, obwohl dies die Leistung höchstwahrscheinlich nicht wesentlich beeinträchtigt: Gruppieren der booleschen Variablen (im Allgemeinen Gruppieren der Variablen des gleichen Typs).
Stefan
1
@stefan: Das gibt es, aber ich habe absichtlich vermieden, den Code stark zu optimieren, und stattdessen (ungefähr) das Minimum getan, das erforderlich ist, um die offensichtlichsten Probleme in der ursprünglichen Implementierung zu beseitigen. Wenn ich wirklich optimieren wollte, würde ich eine #pragma ompund (vielleicht) ein wenig Arbeit hinzufügen , um sicherzustellen, dass jede Schleifeniteration unabhängig ist. Das würde ziemlich wenig Arbeit erfordern, um eine ~ Nx-Beschleunigung zu erhalten, wobei N die Anzahl der verfügbaren Prozessorkerne ist.
Jerry Coffin
Guter Punkt. Dies ist gut genug für eine Antwort auf diese Frage
Stefan
Wie sind 181 Zeiteinheiten 35% langsamer als 0,135 Zeiteinheiten und 16% langsamer als 0,156 Zeiteinheiten? Meinten Sie, dass die Dauer der Java-Version 0,181 beträgt?
Jamesdlin
1
@jamesdlin: Sie verwenden verschiedene Einheiten (so belassen, weil es so war wie im Original). Der C ++ - Code gibt die Zeit in Sekunden an, der Java-Code die Zeit in Millisekunden.
Jerry Coffin
9

Ich vermute, es geht um die Zuweisung von Speicher.

Ich denke, dass Javabeim Programmstart ein großer zusammenhängender Block erfasst wird, während C++das Betriebssystem im Laufe der Zeit nach Kleinigkeiten gefragt wird.

Um diese Theorie auf den Prüfstand zu stellen, habe ich eine Änderung an der C++Version vorgenommen und sie lief plötzlich etwas schneller als die JavaVersion:

int main() {
    {
        // grab a large chunk of contiguous memory and liberate it
        std::vector<double> alloc(20000 * 20);
    }
    FloodIsolation isolation;
    clock_t start = clock();
    for (int i = 0; i < 400; ++i) {
        if(i % 100 == 0) {
            std::cout << i << "\n";
        }
        isolation.isUpdateNeeded();
    }
    clock_t stop = clock();
    std::cout << "Time: " << (1000 * difftime(stop, start) / CLOCKS_PER_SEC) << "\n";
}

Laufzeit ohne Vorbelegungsvektor:

0
100
200
300
Time: 1250.31

Laufzeit mit dem Vorbelegungsvektor:

0
100
200
300
Time: 331.214

Laufzeit für JavaVersion:

0
100
200
300
Time: 407
Galik
quelle
Darauf kann man sich nicht wirklich verlassen. Die Daten in FloodIsolationkönnen noch an anderer Stelle zugewiesen werden.
Stefan
@stefan Immer noch ein interessantes Ergebnis.
Kapitän Giraffe
@ CaptainGiraffe ist es, ich habe nicht gesagt, dass es nutzlos ist ;-)
Stefan
2
@stefan Ich schlage es nicht als Lösung vor, sondern untersuche nur, was ich für das Problem halte. Es scheint, dass es nichts mit Caching zu tun hat, sondern wie sich das C ++ RTS von Java unterscheidet.
Galik
1
@Galik Es ist nicht immer die Ursache, obwohl es ziemlich interessant ist zu sehen, dass es so große Auswirkungen auf Ihre Plattform hat. Auf ideone kann ich Ihr Ergebnis nicht reproduzieren (anscheinend wird der zugewiesene Block nicht wiederverwendet): ideone.com/im4NMO Der Vektor der Strukturlösung hat jedoch eine konsistentere Auswirkung auf die Leistung: ideone.com/b0VWSN
stefan