Java 8 volte più veloce con gli array rispetto a std :: vector in C ++. Cos'ho fatto di sbagliato?


88

Ho il seguente codice Java con diversi grandi array che non cambiano mai le loro dimensioni. Funziona in 1100 ms sul mio computer.

Ho implementato lo stesso codice in C ++ e l'ho usato std::vector.

Il tempo dell'implementazione C ++ che esegue lo stesso identico codice è di 8800 ms sul mio computer. Cosa ho fatto di sbagliato, in modo che funzioni lentamente?

Fondamentalmente il codice fa quanto segue:

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;
}

Itera attraverso diversi array con una dimensione di circa 20000.

Puoi trovare entrambe le implementazioni ai seguenti link:

(Su ideone potevo eseguire il loop solo 400 volte invece di 2000 volte a causa del limite di tempo. Ma anche qui c'è una differenza di tre volte)


42
std::vector<bool>usa un bit per elemento per risparmiare spazio, il che porta a molti spostamenti di bit. Se vuoi la velocità, dovresti starne alla larga. Usa std::vector<int>invece.
molbdnilo

44
@molbdnilo Oppure std :: vector <char>. Non c'è bisogno di sprecare così tanto ;-)
stefan

7
Stranamente abbastanza. La versione c ++ è più veloce quando il numero di celle è 200. Località della cache?
Captain Giraffe

9
Parte II: sarebbe molto meglio creare una classe / struttura separata che contiene uno di ciascun membro degli array e quindi avere un singolo array di oggetti di questa struttura, perché in realtà stai iterando nella memoria solo una volta, in una direzione.
Timo Geusch

9
@TimoGeusch: Anche se penso che h[i] += 1;o (meglio ancora) ++h[i]sia più leggibile di h[i] = h[i] + 1;, sarei un po 'sorpreso di vedere una differenza significativa di velocità tra di loro. Un compilatore può "capire" che stanno entrambi facendo la stessa cosa e generare lo stesso codice in entrambi i casi (almeno nei casi più comuni).
Jerry Coffin

Risposte:


36

Ecco la versione C ++ con i dati per nodo raccolti in una struttura e un singolo vettore di quella struttura utilizzato:

#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";
}

esempio dal vivo

Il tempo è ora il doppio della velocità della versione Java. (846 contro 1631).

Le probabilità sono che il JIT abbia notato la cache che brucia l'accesso ai dati ovunque e abbia trasformato il tuo codice in un ordine logicamente simile ma più efficiente.

Ho anche disattivato la sincronizzazione stdio, in quanto è necessaria solo se mescoli printf/ scanfcon C ++ std::coute std::cin. Come accade, si stampano solo pochi valori, ma il comportamento predefinito di C ++ per la stampa è eccessivamente paranoico e inefficiente.

Se nEdgesnon è un valore costante effettivo, i 3 valori "array" dovranno essere eliminati dal struct. Ciò non dovrebbe causare un enorme calo delle prestazioni.

Potresti essere in grado di ottenere un altro aumento delle prestazioni ordinando i valori in quello structdiminuendo le dimensioni, riducendo così l'impronta di memoria (e ordinando anche l'accesso quando non importa). Ma non sono sicuro.

Una regola pratica è che un singolo errore di cache è 100 volte più costoso di un'istruzione. Organizzare i dati in modo da avere la coerenza della cache ha molto valore.

Se non è possibile riorganizzare i dati in a struct, è possibile modificare l'iterazione in modo che si trovi su ogni contenitore a turno.

Per inciso, nota che le versioni Java e C ++ presentavano alcune sottili differenze. Quello che ho notato è che la versione Java ha 3 variabili nel ciclo "for each edge", mentre quella C ++ ne aveva solo 2. Ho fatto corrispondere la mia a Java. Non so se ce ne sono altri.


44

Sì, la cache nella versione c ++ prende un martello. Sembra che il JIT sia meglio equipaggiato per gestire questo.

Se modifichi l'esterno forin isUpdateNeeded () in frammenti più brevi. La differenza se ne va.

L'esempio seguente produce una velocità 4x.

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;
        }
    }

}

Ciò mostra in misura ragionevole che i mancati riscontri nella cache sono la ragione del rallentamento. È anche importante notare che le variabili non dipendono, quindi è possibile creare facilmente una soluzione filettata.

Ordine ripristinato

Come da commento di stefans ho provato a raggrupparli in una struttura usando le dimensioni originali. Ciò rimuove la pressione immediata della cache in modo simile. Il risultato è che la versione c ++ (CCFLAG -O3) è circa il 15% più veloce della versione java.

Varning né corto né carino.

#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";
}
                                                                              

Il mio risultato differisce leggermente da Jerry Coffins per le dimensioni originali. Per me le differenze rimangono. Potrebbe benissimo essere la mia versione Java, 1.7.0_75.


12
Potrebbe essere una buona idea raggruppare quei dati in una struttura e avere solo un vettore
stefan

Beh, sono sul cellulare quindi non posso effettuare misurazioni ;-) ma l'unico vettore dovrebbe essere buono (anche in termini di allocazioni)
stefan

1
Usare l' ++aiuto a qualsiasi titolo? x = x + 1sembra terribilmente goffo rispetto a ++x.
tadman

3
Correggi la parola errata "risultato". Mi sta uccidendo .. :)
fleetC0m

1
Se l'intero iteratore rientra in un singolo registro, in alcuni casi la creazione di una copia potrebbe essere più veloce rispetto all'aggiornamento sul posto. Se stai aggiornando sul posto, questo è perché molto probabilmente stai usando il valore aggiornato subito dopo. Quindi hai una dipendenza Lettura dopo scrittura. Se si aggiorna, ma è necessario solo il vecchio valore, queste operazioni non dipendono l'una dall'altra e la CPU ha più spazio per eseguirle in parallelo, ad esempio su pipeline diverse, aumentando l'IPC effettivo.
Piotr Kołaczkowski

20

Come @Stefan ha intuito in un commento alla risposta di @ CaptainGiraffe, si guadagna un bel po 'usando un vettore di strutture invece di una struttura di vettori. Il codice corretto ha questo aspetto:

#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";
}

Compilato con il compilatore da VC ++ 2015 CTP, utilizzando -EHsc -O2b2 -GL -Qpar, ottengo risultati come:

0
100
200
300
Time: 0.135

La compilazione con g ++ produce un risultato leggermente più lento:

0
100
200
300
Time: 0.156

Sullo stesso hardware, utilizzando il compilatore / JVM da Java 8u45, ottengo risultati come:

0
100
200
300
Time: 181

Questo è circa il 35% più lento della versione da VC ++ e circa il 16% più lento della versione da g ++.

Se aumentiamo il numero di iterazioni al 2000 desiderato, la differenza scende solo al 3%, suggerendo che parte del vantaggio del C ++ in questo caso è semplicemente un caricamento più veloce (un problema perenne con Java), non proprio nell'esecuzione stessa. Ciò non mi sorprende in questo caso: il calcolo misurato (nel codice pubblicato) è così banale che dubito che la maggior parte dei compilatori possa fare molto per ottimizzarlo.


1
C'è ancora spazio per miglioramenti anche se molto probabilmente non influirà in modo significativo sulle prestazioni: raggruppare le variabili booleane (in generale raggruppare le variabili dello stesso tipo).
stefan

1
@stefan: C'è, ma stavo intenzionalmente evitando di fare qualsiasi pesante ottimizzazione del codice, e invece di fare (approssimativamente) il minimo necessario per rimuovere i problemi più evidenti nell'implementazione originale. Se volessi davvero ottimizzare, aggiungerei un #pragma ompe (forse) un po 'di lavoro per garantire che ogni iterazione del ciclo sia indipendente. Ciò richiederebbe un lavoro abbastanza minimo per ottenere un aumento della velocità di ~ Nx, dove N è il numero di core del processore disponibili.
Jerry Coffin

Buon punto. Questo è abbastanza buono per una risposta a questa domanda
stefan

In che modo 181 unità di tempo sono il 35% più lente di 0,135 unità di tempo e il 16% più lente di 0,156 unità di tempo? Volevi dire che la durata della versione Java è 0.181?
jamesdlin

1
@jamesdlin: stanno usando unità diverse (lasciate così, perché è come erano le cose nell'originale). Il codice C ++ dà il tempo in secondi, ma il codice Java dà il tempo in millisecondi.
Jerry Coffin

9

Sospetto che si tratti di allocazione della memoria.

Sto pensando che Javaafferra un grande blocco contiguo all'avvio del programma mentre C++chiede al sistema operativo bit e pezzi mentre procede.

Per mettere alla prova questa teoria ho apportato una modifica alla C++versione e improvvisamente ha iniziato a funzionare leggermente più velocemente della Javaversione:

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";
}

Runtime senza il vettore di preallocazione:

0
100
200
300
Time: 1250.31

Runtime con il vettore di preallocazione:

0
100
200
300
Time: 331.214

Runtime per Java versione:

0
100
200
300
Time: 407

Beh, non puoi davvero fare affidamento su questo. I dati in FloodIsolationpossono ancora essere allocati altrove.
stefan

@stefan Ancora un risultato interessante.
Captain Giraffe

@CaptainGiraffe è, non ho detto che è inutile ;-)
stefan

2
@stefan Non lo sto proponendo come soluzione, ma semplicemente indagando su quello che penso sia il problema. Sembra che potrebbe non avere nulla a che fare con la memorizzazione nella cache, ma in che modo C ++ RTS differisce da Java.
Galik

1
@Galik Non è sempre la causa, anche se è abbastanza interessante vedere che ha un grande impatto sulla tua piattaforma. Su ideone, non riesco a riprodurre il tuo risultato (come sembra, il blocco allocato non viene riutilizzato): ideone.com/im4NMO Tuttavia, il vettore della soluzione structs ha un impatto sulle prestazioni più coerente: ideone.com/b0VWSN
stefan
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.