Java 8 kali lebih cepat dengan array daripada std :: vector di C ++. Apa kesalahan yang telah aku perbuat?

88

Saya memiliki kode Java berikut dengan beberapa array besar yang tidak pernah mengubah ukurannya. Ini berjalan dalam 1100 ms di komputer saya.

Saya menerapkan kode yang sama di C ++ dan digunakan std::vector.

Waktu implementasi C ++ yang menjalankan kode yang sama persis adalah 8800 ms di komputer saya. Apa yang saya lakukan salah, sehingga berjalan lambat?

Pada dasarnya kode melakukan hal berikut:

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

Ini mengulangi melalui array yang berbeda dengan ukuran sekitar 20000.

Anda dapat menemukan kedua penerapan di bawah tautan berikut:

(Pada ideone saya hanya dapat menjalankan loop 400 kali, bukan 2000 kali karena batasan waktu. Tetapi bahkan di sini ada perbedaan tiga kali)

RobinXSI
sumber
42
std::vector<bool>menggunakan satu bit per elemen untuk menghemat ruang, yang menyebabkan banyak pergeseran bit. Jika Anda menginginkan kecepatan, Anda harus menjauhinya. Gunakan std::vector<int>sebagai gantinya.
molbdnilo
44
@molbdnilo Atau std :: vector <char>. Tidak perlu membuang-buang bahwa banyak ;-)
Stefan
7
Cukup lucu. Versi c ++ lebih cepat jika jumlah selnya 200. Lokalitas cache?
Kapten Giraffe
9
Bagian II: Anda akan jauh lebih baik membuat kelas / struct terpisah yang berisi salah satu dari setiap anggota array dan kemudian memiliki satu array objek dari struct ini, karena kemudian Anda sebenarnya mengulangi melalui memori hanya sekali, di satu arah.
Timo Geusch
9
@TimoGeusch: Meskipun menurut saya h[i] += 1;atau (lebih baik lagi) ++h[i]lebih mudah dibaca daripada h[i] = h[i] + 1;, saya agak terkejut melihat perbedaan yang signifikan dalam kecepatan di antara keduanya. Kompiler dapat "mengetahui" bahwa keduanya melakukan hal yang sama, dan menghasilkan kode yang sama dengan cara apa pun (setidaknya dalam kebanyakan kasus umum).
Jerry Coffin

Jawaban:

36

Berikut adalah versi C ++ dengan data per node dikumpulkan ke dalam sebuah struktur, dan satu vektor dari struktur tersebut digunakan:

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

contoh hidup

Waktu sekarang 2x kecepatan versi Java. (846 vs 1631).

Kemungkinannya adalah JIT memperhatikan pembakaran cache dari mengakses data di semua tempat, dan mengubah kode Anda menjadi urutan yang serupa secara logis tetapi lebih efisien.

Saya juga mematikan sinkronisasi stdio, karena itu hanya diperlukan jika Anda mencampur printf/ scanfdengan C ++ std::coutdan std::cin. Kebetulan, Anda hanya mencetak beberapa nilai, tetapi perilaku default C ++ untuk pencetakan terlalu paranoid dan tidak efisien.

Jika nEdgesbukan nilai konstanta sebenarnya, maka 3 nilai "array" harus dihilangkan dari struct. Itu seharusnya tidak menyebabkan kinerja yang besar.

Anda mungkin bisa mendapatkan peningkatan kinerja lain dengan mengurutkan nilai-nilai di structdalamnya dengan mengurangi ukuran, sehingga mengurangi jejak memori (dan juga menyortir akses ketika tidak menjadi masalah). Tapi saya tidak yakin.

Aturan praktisnya adalah bahwa satu cache miss 100x lebih mahal daripada sebuah instruksi. Mengatur data Anda agar memiliki koherensi cache memiliki banyak nilai.

Jika menata ulang data menjadi a structtidak dapat dilakukan, Anda dapat mengubah iterasi agar berada di atas setiap penampung secara bergantian.

Sebagai tambahan, perhatikan bahwa versi Java dan C ++ memiliki beberapa perbedaan kecil di dalamnya. Yang saya lihat adalah bahwa versi Java memiliki 3 variabel dalam loop "untuk setiap tepi", sedangkan C ++ hanya memiliki 2. Saya membuat milik saya cocok dengan Java. Saya tidak tahu apakah ada yang lain.

Yakk - Adam Nevraumont
sumber
44

Ya, cache dalam versi c ++ membutuhkan palu. Tampaknya JIT lebih siap untuk menangani ini.

Jika Anda mengubah bagian luar fordi isUpdateNeeded () menjadi cuplikan yang lebih pendek. Perbedaannya hilang.

Contoh di bawah ini menghasilkan percepatan 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;
        }
    }

}

Ini menunjukkan pada tingkat yang wajar bahwa cache miss adalah alasan perlambatan. Penting juga untuk dicatat bahwa variabel tidak tergantung sehingga solusi berulir mudah dibuat.

Pesanan dipulihkan

Sesuai komentar Stefans, saya mencoba mengelompokkan mereka dalam sebuah struct menggunakan ukuran aslinya. Ini menghapus tekanan cache langsung dengan cara yang sama. Hasilnya adalah versi c ++ (CCFLAG -O3) sekitar 15% lebih cepat daripada versi java.

Varning tidak pendek atau cantik.

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

Hasil saya sedikit berbeda dari Jerry Coffins untuk ukuran aslinya. Bagi saya perbedaannya tetap ada. Mungkin saja versi java saya, 1.7.0_75.

Kapten Giraffe
sumber
12
Mungkin ide yang bagus untuk mengelompokkan data itu dalam sebuah struct dan hanya memiliki satu vektor
Stefan
Saya menggunakan ponsel jadi saya tidak bisa melakukan pengukuran ;-) tetapi satu vektor seharusnya bagus (juga dalam hal alokasi)
Stefan
1
Apakah menggunakan ++bantuan dalam kapasitas apa pun? x = x + 1tampaknya sangat kikuk dibandingkan dengan ++x.
tadman
3
Harap perbaiki kata "hasil" yang salah eja. Ini membunuh saya .. :)
fleetC0m
1
Jika seluruh iterator cocok dalam satu register, maka membuat salinan mungkin sebenarnya lebih cepat dalam beberapa kasus daripada memperbarui di tempat. Jika Anda melakukan pembaruan di tempat, ini karena kemungkinan besar Anda menggunakan nilai yang diperbarui setelahnya. Jadi, Anda memiliki dependensi Baca-setelah-Tulis. Jika Anda mengupdate, tetapi hanya membutuhkan nilai lama, operasi tersebut tidak bergantung satu sama lain dan CPU memiliki lebih banyak ruang untuk melakukannya secara paralel, misalnya pada pipeline yang berbeda, meningkatkan IPC efektif.
Piotr Kołaczkowski
20

Seperti yang @Stefan tebak dalam komentar pada jawaban @ CaptainGiraffe, Anda mendapatkan cukup banyak dengan menggunakan vektor struct bukan struct vektor. Kode yang diperbaiki terlihat seperti ini:

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

Dikompilasi dengan kompiler dari VC ++ 2015 CTP, menggunakan -EHsc -O2b2 -GL -Qpar, saya mendapatkan hasil seperti:

0
100
200
300
Time: 0.135

Mengompilasi dengan g ++ menghasilkan hasil yang sedikit lebih lambat:

0
100
200
300
Time: 0.156

Pada perangkat keras yang sama, menggunakan compiler / JVM dari Java 8u45, saya mendapatkan hasil seperti:

0
100
200
300
Time: 181

Ini sekitar 35% lebih lambat dari versi dari VC ++, dan sekitar 16% lebih lambat dari versi dari g ++.

Jika kita meningkatkan jumlah iterasi ke 2000 yang diinginkan, perbedaannya turun menjadi hanya 3%, menunjukkan bahwa bagian dari keuntungan C ++ dalam hal ini hanyalah memuat lebih cepat (masalah abadi dengan Java), tidak benar-benar dalam eksekusi itu sendiri. Hal ini tidak mengejutkan saya dalam kasus ini - perhitungan yang diukur (dalam kode yang diposting) sangat sepele sehingga saya ragu sebagian besar kompiler dapat melakukan banyak hal untuk mengoptimalkannya.

Jerry Coffin
sumber
1
Masih ada ruang untuk perbaikan meskipun ini kemungkinan besar tidak akan memengaruhi kinerja secara signifikan: mengelompokkan variabel boolean (secara umum mengelompokkan variabel dengan jenis yang sama).
Stefan
1
@ Stefan: Ada, tetapi saya sengaja menghindari melakukan pengoptimalan kode yang berat, dan sebaliknya melakukan (secara kasar) minimum yang diperlukan untuk menghapus masalah yang paling jelas dalam implementasi asli. Jika saya benar-benar ingin mengoptimalkan, saya akan menambahkan #pragma omp, dan (mungkin) sedikit pekerjaan untuk memastikan setiap iterasi loop independen. Itu akan membutuhkan kerja yang cukup minimal untuk mendapatkan kecepatan ~ Nx, di mana N adalah jumlah inti prosesor yang tersedia.
Jerry Coffin
Poin yang bagus. Ini cukup baik untuk menjawab pertanyaan ini
Stefan
Bagaimana 181 satuan waktu 35% lebih lambat dari satuan waktu 0,135 dan 16% lebih lambat dari satuan waktu 0,156? Apakah maksud Anda durasi versi Java adalah 0.181?
jamesdlin
1
@jamesdlin: mereka menggunakan unit yang berbeda (dibiarkan begitu, karena begitulah aslinya). Kode C ++ memberikan waktu dalam hitungan detik, tetapi kode Java memberikan waktu dalam milidetik.
Jerry Coffin
9

Saya menduga ini tentang alokasi memori.

Saya berpikir bahwa Javamengambil blok bersebelahan besar saat startup program sambil C++meminta OS untuk potongan-potongan saat berjalan.

Untuk menguji teori itu, saya membuat satu modifikasi pada C++versi dan tiba-tiba mulai berjalan sedikit lebih cepat dari Javaversi:

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 tanpa vektor pra-alokasi:

0
100
200
300
Time: 1250.31

Runtime dengan vektor yang dialokasikan sebelumnya:

0
100
200
300
Time: 331.214

Runtime untuk Javaversi:

0
100
200
300
Time: 407
Galik
sumber
Anda tidak bisa mengandalkan itu. Data di FloodIsolationmungkin masih dialokasikan di tempat lain.
Stefan
@stefan Masih hasil yang menarik.
Kapten Giraffe
@CaptainGiraffe itu, saya tidak mengatakan itu tidak berguna ;-)
Stefan
2
@stefan Saya tidak mengusulkannya sebagai solusi, hanya menyelidiki apa yang menurut saya masalahnya. Tampaknya ini mungkin tidak ada hubungannya dengan caching tetapi bagaimana C ++ RTS berbeda dari Java.
Galik
1
@Galik Itu tidak selalu menjadi penyebabnya, meskipun cukup menarik untuk melihatnya berdampak besar pada platform Anda. Pada ideone, saya tidak dapat mereproduksi hasil Anda (sepertinya, blok yang dialokasikan tidak digunakan kembali): ideone.com/im4NMO Namun, vektor solusi struct memiliki dampak kinerja yang lebih konsisten: ideone.com/b0VWSN
stefan