Java nhanh hơn 8 lần với mảng so với std :: vector trong C ++. Tôi đã làm gì sai?


88

Tôi có đoạn mã Java sau với một số mảng lớn không bao giờ thay đổi kích thước của chúng. Nó chạy trong 1100 ms trên máy tính của tôi.

Tôi đã triển khai mã tương tự trong C ++ và đã sử dụng std::vector.

Thời gian triển khai C ++ chạy cùng một mã là 8800 mili giây trên máy tính của tôi. Tôi đã làm gì sai để nó chạy chậm thế này?

Về cơ bản, mã thực hiện như sau:

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

Nó lặp qua các mảng khác nhau với kích thước khoảng 20000.

Bạn có thể tìm thấy cả hai cách triển khai theo các liên kết sau:

(Trên Ideone, tôi chỉ có thể chạy vòng lặp 400 lần thay vì 2000 lần vì giới hạn thời gian. Nhưng ngay cả ở đây cũng có sự khác biệt là ba lần)


42
std::vector<bool>sử dụng một bit cho mỗi phần tử để tiết kiệm dung lượng, dẫn đến nhiều sự thay đổi bit. Nếu bạn muốn tốc độ, bạn nên tránh xa nó. Sử dụng std::vector<int>thay thế.
molbdnilo

44
@molbdnilo Hoặc std :: vector <char>. Không cần phải lãng phí nhiều ;-)
stefan

7
Đủ vui rồi. Phiên bản c ++ nhanh hơn khi số lượng ô là 200. Cache local?
Captain Giraffe,

9
Phần II: Tốt hơn hết bạn nên tạo một lớp / cấu trúc riêng biệt chứa một trong mỗi thành viên của mảng và sau đó có một mảng đối tượng duy nhất của cấu trúc này, bởi vì sau đó bạn thực sự chỉ lặp lại bộ nhớ một lần, trong một chiều.
Timo Geusch

9
@TimoGeusch: Mặc dù tôi nghĩ h[i] += 1;hoặc (vẫn tốt hơn) ++h[i]dễ đọc hơn h[i] = h[i] + 1;, nhưng tôi hơi ngạc nhiên khi thấy bất kỳ sự khác biệt đáng kể nào về tốc độ giữa chúng. Một trình biên dịch có thể "tìm ra" rằng cả hai đều đang làm cùng một việc và tạo ra cùng một mã theo cách nào đó (ít nhất là trong hầu hết các trường hợp phổ biến).
Jerry Coffin

Câu trả lời:


36

Đây là phiên bản C ++ với dữ liệu mỗi nút được tập hợp thành một cấu trúc và một vectơ duy nhất của cấu trúc đó được sử dụng:

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

ví dụ trực tiếp

Bây giờ tốc độ của phiên bản Java gấp 2 lần. (846 so với 1631).

Tỷ lệ cược là JIT nhận thấy việc ghi bộ nhớ cache của việc truy cập dữ liệu ở khắp nơi và chuyển đổi mã của bạn thành một thứ tự logic tương tự nhưng hiệu quả hơn.

Tôi cũng đã tắt đồng bộ hóa stdio, vì điều đó chỉ cần thiết nếu bạn kết hợp printf/ scanfvới C ++ std::coutstd::cin. Khi nó xảy ra, bạn chỉ in ra một vài giá trị, nhưng hành vi mặc định của C ++ để in là quá hoang tưởng và không hiệu quả.

Nếu nEdgeskhông phải là một giá trị hằng số thực tế, thì 3 giá trị "mảng" sẽ phải bị loại bỏ khỏi struct. Điều đó sẽ không gây ra một hiệu suất lớn.

Bạn có thể có được một mức tăng hiệu suất khác bằng cách sắp xếp các giá trị trong đó structtheo cách giảm kích thước, do đó giảm dung lượng bộ nhớ (và sắp xếp cả quyền truy cập khi nó không quan trọng). Nhưng tôi không chắc.

Một nguyên tắc chung là một lần bỏ lỡ bộ nhớ cache sẽ đắt hơn 100 lần so với một lệnh. Sắp xếp dữ liệu của bạn để có đồng tiền trong bộ nhớ cache có rất nhiều giá trị.

Nếu việc sắp xếp lại dữ liệu thành a structlà không khả thi, bạn có thể thay đổi lần lặp lại của mình để lần lượt qua từng vùng chứa.

Ngoài ra, hãy lưu ý rằng phiên bản Java và C ++ có một số khác biệt nhỏ trong đó. Điều tôi phát hiện ra là phiên bản Java có 3 biến trong vòng lặp "cho mỗi cạnh", trong khi phiên bản C ++ chỉ có 2. Tôi đã thực hiện khớp với Java. Tôi không biết nếu có những người khác.


44

Đúng vậy, bộ nhớ cache trong phiên bản c ++ cần một cái búa. Có vẻ như JIT được trang bị tốt hơn để xử lý điều này.

Nếu bạn thay đổi phần bên ngoài fortrong isUpdateNeeded () thành các đoạn mã ngắn hơn. Sự khác biệt biến mất.

Mẫu bên dưới tạo ra tốc độ gấp 4 lần.

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

}

Điều này cho thấy ở một mức độ hợp lý rằng bộ nhớ cache bị bỏ sót là lý do gây ra sự chậm lại. Cũng cần lưu ý rằng các biến không phụ thuộc nên một giải pháp phân luồng dễ dàng được tạo ra.

Đã khôi phục đơn hàng

Theo nhận xét của stefans, tôi đã thử nhóm chúng trong một cấu trúc bằng cách sử dụng kích thước ban đầu. Điều này loại bỏ áp lực bộ nhớ cache tức thì theo cách tương tự. Kết quả là phiên bản c ++ (CCFLAG -O3) nhanh hơn khoảng 15% so với phiên bản java.

Varning không ngắn cũng không đẹp.

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

Kết quả của tôi hơi khác với Jerry Coffins cho các kích thước ban đầu. Đối với tôi, sự khác biệt vẫn còn. Nó cũng có thể là phiên bản java của tôi, 1.7.0_75.


12
Có thể là một ý tưởng hay khi nhóm dữ liệu đó trong một cấu trúc và chỉ có một vectơ
stefan

Vâng, tôi đang trên điện thoại di động vì vậy tôi không thể thực hiện đo đạc ;-) nhưng vector ta nên được tốt (còn về phân bổ)
stefan

1
Sử dụng có ++trợ giúp trong bất kỳ khả năng? x = x + 1có vẻ như khủng khiếp so với ++x.
tadman

3
Hãy sửa từ sai chính tả "kết quả". Nó đang giết tôi .. :)
hạm độiC0m

1
Nếu toàn bộ trình lặp phù hợp với một thanh ghi, thì việc tạo bản sao trong một số trường hợp có thể thực sự nhanh hơn so với cập nhật tại chỗ. Nếu bạn đang cập nhật tại chỗ, điều này là do bạn rất có thể đang sử dụng giá trị được cập nhật ngay sau đó. Vì vậy, bạn có một phụ thuộc Read-after-Write. Nếu bạn cập nhật, nhưng chỉ cần giá trị cũ, các hoạt động đó không phụ thuộc vào nhau và CPU có nhiều chỗ hơn để thực hiện chúng song song, ví dụ trên các đường ống khác nhau, tăng IPC hiệu quả.
Piotr Kołaczkowski

20

Như @Stefan đã đoán trong một bình luận về câu trả lời của @ CaptainGiraffe, bạn thu được kha khá khi sử dụng vectơ cấu trúc thay vì cấu trúc vectơ. Mã đã sửa có dạng như sau:

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

Được biên dịch bằng trình biên dịch từ VC ++ 2015 CTP, bằng cách sử dụng -EHsc -O2b2 -GL -Qpar, tôi nhận được kết quả như:

0
100
200
300
Time: 0.135

Biên dịch với g ++ tạo ra kết quả hơi chậm hơn:

0
100
200
300
Time: 0.156

Trên cùng một phần cứng, sử dụng trình biên dịch / JVM từ Java 8u45, tôi nhận được kết quả như:

0
100
200
300
Time: 181

Điều này chậm hơn khoảng 35% so với phiên bản từ VC ++ và chậm hơn khoảng 16% so với phiên bản từ g ++.

Nếu chúng ta tăng số lần lặp đến 2000 mong muốn, sự khác biệt giảm xuống chỉ còn 3%, cho thấy rằng một phần lợi thế của C ++ trong trường hợp này chỉ đơn giản là tải nhanh hơn (một vấn đề lâu năm với Java), không thực sự nằm trong chính việc thực thi. Điều này không làm tôi ngạc nhiên trong trường hợp này - tính toán được đo lường (trong mã đã đăng) quá tầm thường đến mức tôi nghi ngờ hầu hết các trình biên dịch có thể làm rất nhiều để tối ưu hóa nó.


1
Vẫn còn chỗ để cải thiện mặc dù điều này rất có thể sẽ không ảnh hưởng đáng kể đến hiệu suất: nhóm các biến boolean (nói chung là nhóm các biến cùng loại).
stefan

1
@stefan: Có, nhưng tôi đã cố ý tránh thực hiện bất kỳ tối ưu hóa nặng nề nào đối với mã, và thay vào đó làm (đại khái) mức tối thiểu cần thiết để loại bỏ các vấn đề rõ ràng nhất trong quá trình triển khai ban đầu. Nếu tôi thực sự muốn tối ưu hóa, tôi sẽ thêm một #pragma omp, và (có lẽ) một chút công việc để đảm bảo mỗi lần lặp vòng lặp là độc lập. Điều đó sẽ mất khá nhiều công việc để có được tốc độ ~ Nx, trong đó N là số lõi bộ xử lý có sẵn.
Jerry Coffin

Điểm tốt. Đây là cũng đủ cho một câu trả lời cho câu hỏi này
stefan

181 đơn vị thời gian chậm hơn 35% so với 0,135 đơn vị thời gian và 16% chậm hơn 0,156 đơn vị thời gian? Ý của bạn là thời lượng của phiên bản Java là 0,181?
jamesdlin

1
@jamesdlin: họ đang sử dụng các đơn vị khác nhau (trái theo cách đó, vì đó là cách mọi thứ trong bản gốc). Mã C ++ cho thời gian tính bằng giây, nhưng mã Java cho thời gian tính bằng mili giây.
Jerry Coffin

9

Tôi nghi ngờ đây là về phân bổ bộ nhớ.

Tôi đang nghĩ rằng Javalấy một khối liền kề lớn khi khởi động chương trình trong khi C++yêu cầu hệ điều hành cung cấp các bit và mảnh khi nó diễn ra.

Để đưa lý thuyết đó vào thử nghiệm, tôi đã thực hiện một sửa đổi đối với C++phiên bản và nó đột nhiên bắt đầu chạy nhanh hơn một chút so với Javaphiên bản:

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

Thời gian chạy không có vectơ định vị trước:

0
100
200
300
Time: 1250.31

Thời gian chạy với vectơ định vị trước:

0
100
200
300
Time: 331.214

Thời gian chạy cho Javaphiên bản:

0
100
200
300
Time: 407

Vâng, bạn không thể thực sự dựa vào đó. Dữ liệu trong FloodIsolationvẫn có thể được phân bổ ở nơi khác.
stefan

@stefan Vẫn là một kết quả thú vị.
Captain Giraffe,

@CaptainGiraffe đúng là như vậy, tôi không nói nó vô dụng ;-)
stefan

2
@stefan Tôi không đề xuất nó như một giải pháp, chỉ đơn thuần là điều tra những gì tôi nghĩ là vấn đề. Có vẻ như nó có thể không liên quan gì đến bộ nhớ đệm nhưng C ++ RTS khác với Java như thế nào.
Galik

1
@Galik Đó không phải lúc nào cũng là nguyên nhân, mặc dù khá thú vị khi thấy nó có tác động lớn đến nền tảng của bạn. Trên Ideone, tôi không thể tạo lại kết quả của bạn (có vẻ như khối được phân bổ không được sử dụng lại): ideone.com/im4NMO Tuy nhiên, giải pháp vectơ của structs có tác động hiệu suất nhất quán hơn: ideone.com/b0VWSN
stefan
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.