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:
- Java: https://ideone.com/R8KqjT
- C ++: https://ideone.com/Lu7RpE
(Pada ideone saya hanya dapat menjalankan loop 400 kali, bukan 2000 kali karena batasan waktu. Tetapi bahkan di sini ada perbedaan tiga kali)
std::vector<bool>
menggunakan satu bit per elemen untuk menghemat ruang, yang menyebabkan banyak pergeseran bit. Jika Anda menginginkan kecepatan, Anda harus menjauhinya. Gunakanstd::vector<int>
sebagai gantinya.h[i] += 1;
atau (lebih baik lagi)++h[i]
lebih mudah dibaca daripadah[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).Jawaban:
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
/scanf
dengan C ++std::cout
danstd::cin
. Kebetulan, Anda hanya mencetak beberapa nilai, tetapi perilaku default C ++ untuk pencetakan terlalu paranoid dan tidak efisien.Jika
nEdges
bukan nilai konstanta sebenarnya, maka 3 nilai "array" harus dihilangkan daristruct
. Itu seharusnya tidak menyebabkan kinerja yang besar.Anda mungkin bisa mendapatkan peningkatan kinerja lain dengan mengurutkan nilai-nilai di
struct
dalamnya 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
struct
tidak 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.
sumber
Ya, cache dalam versi c ++ membutuhkan palu. Tampaknya JIT lebih siap untuk menangani ini.
Jika Anda mengubah bagian luar
for
di 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.
sumber
++
bantuan dalam kapasitas apa pun?x = x + 1
tampaknya sangat kikuk dibandingkan dengan++x
.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.
sumber
#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.Saya menduga ini tentang alokasi memori.
Saya berpikir bahwa
Java
mengambil blok bersebelahan besar saat startup program sambilC++
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 dariJava
versi: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
Java
versi:0 100 200 300 Time: 407
sumber
FloodIsolation
mungkin masih dialokasikan di tempat lain.