JavaはC ++のstd :: vectorよりも配列の方が8倍高速です。何を間違えたのですか?


88

サイズが変更されないいくつかの大きな配列を持つ次のJavaコードがあります。私のコンピューターでは1100ミリ秒で実行されます。

同じコードをC ++に実装して使用しましたstd::vector

まったく同じコードを実行するC ++実装の時間は、私のコンピューターでは8800ミリ秒です。これがゆっくり実行されるように、私は何を間違えましたか?

基本的に、コードは次のことを行います。

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

サイズが約20000のさまざまな配列を反復処理します。

次のリンクの下に両方の実装があります。

(ideoneでは、時間制限のため、ループを2000回ではなく400回しか実行できませんでした。ただし、ここでも3回の違いがあります)


42
std::vector<bool>要素ごとに1ビットを使用してスペースを節約するため、多くのビットシフトが発生します。スピードが欲しいなら、それから離れるべきです。std::vector<int>代わりに使用してください。
molbdnilo 2015

44
@molbdniloまたはstd :: vector <char>。無駄にする必要はありませんその多くが;-)
ステファン・

7
おかしなことに十分です。セルの数が200の場合、c ++バージョンはより高速になります。
キリン船長、

9
パートII:配列の各メンバーの1つを含む個別のクラス/構造体を作成し、この構造体のオブジェクトの単一の配列を作成する方がはるかに良いでしょう。ひとつの方向。
Timo Geusch 2015

9
@TimoGeusch:h[i] += 1;または(より良い)の方++h[i]がより読みやすいh[i] = h[i] + 1;と思いますが、両者の速度に大きな違いがあるとは少し驚きます。コンパイラは、両方が同じことを実行していることを「把握」して、どちらかの方法で同じコードを生成できます(少なくともほとんどの場合)。
Jerry Coffin

回答:


36

これは、ノードごとのデータが構造に収集され、その構造の単一のベクトルが使用されるC ++バージョンです。

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

実例

時間は、Javaバージョンの2倍の速度になりました。(846 vs 1631)。

奇妙なことに、JITはあらゆる場所でデータにアクセスするキャッシュの書き込みに気づき、コードを論理的に類似しているがより効率的な順序に変換しました。

stdio同期もオフにしました。これは、printf/ scanfとC ++ std::coutおよびを混在させる場合にのみ必要であるためですstd::cin。たまたま、出力するのは少数の値だけですが、C ++のデフォルトの印刷動作は過度に偏執的で非効率的です。

nEdgesが実際の定数値でない場合は、3つの「配列」値をから取り除く必要がありstructます。それがパフォーマンスに大きな影響を与えることはありません。

その中の値をソートすることにより、別のパフォーマンス向上を得ることができるかもしれません structサイズを小さくすることでメモリフットプリントを削減することで、(また、重要でない場合はアクセスもソートします)。しかし、私にはわかりません。

経験則では、1つのキャッシュミスは、命令よりも100倍高価です。キャッシュの一貫性を保つようにデータを調整することには多くの価値があります。

データをに再配置structすることが不可能な場合は、反復を変更して各コンテナーを順番に繰り返します。

余談ですが、JavaとC ++のバージョンには微妙な違いがあることに注意してください。私が見つけたのは、Javaバージョンには「for each edge」ループに3つの変数があるのに対し、C ++には2つしかなかったということです。私はJavaと一致させました。他にあるかわかりません。


44

うん、C ++バージョンのキャッシュはハンマーがかかります。JITはこれを処理するためによりよく装備されているようです。

forisUpdateNeeded()のアウターを短いスニペットに変更した場合。違いはなくなります。

以下のサンプルは4倍のスピードアップを生成します。

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

}

これは、キャッシュミスがスローダウンの原因であることをある程度示しています。また、変数は依存しないため、スレッド化されたソリューションを簡単に作成できることに注意することも重要です。

注文が復元されました

ステファンのコメントに従って、元のサイズを使用して構造体にグループ化してみました。これにより、同様の方法で即時のキャッシュ負荷が軽減されます。その結果、c ++(CCFLAG -O3)バージョンは、javaバージョンよりも約15%高速です。

短くもきれいにも変化しません。

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

私の結果は、元のサイズのJerry Coffinsとは少し異なります。私には違いが残っています。それは私のJavaバージョン1.7.0_75かもしれません。


12
そのデータを構造体にグループ化し、1つのベクトルのみを持つことをお勧めします
stefan

まあ私はモバイルなので、測定はできません;-)しかし、1つのベクトルは(割り当ての点でも)良いはずです
stefan

1
++何かの助けを借りて使用していますか?x = x + 1に比べてひどく不格好なよう++xです。
tadman 2015

3
スペルミスのある「結果」を修正してください。それは私を殺しています.. :)
fleetC0m

1
イテレータ全体が1つのレジスタに収まる場合、コピーを作成する方が、実際に更新するよりも実際には高速な場合があります。更新を適切に行う場合は、更新された値を直後に使用する可能性が高いためです。したがって、Read-after-Write依存関係があります。更新しても古い値のみが必要な場合、これらの操作は互いに依存せず、CPUは、たとえば異なるパイプラインでそれらを並行して実行する余地があり、効果的なIPCが向上します。
PiotrKołaczkowski15年

20

@CaptainGiraffeの回答に関するコメントで@Stefanが推測したように、構造体のベクトルではなく構造体のベクトルを使用することで、かなりの利益が得られます。修正されたコードは次のようになります。

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

VC ++ 2015 CTPのコンパイラでコンパイルし、を使用して-EHsc -O2b2 -GL -Qpar、次のような結果が得られます。

0
100
200
300
Time: 0.135

g ++でコンパイルすると、少し遅い結果が生成されます。

0
100
200
300
Time: 0.156

同じハードウェアで、Java 8u45のコンパイラ/ JVMを使用すると、次のような結果が得られます。

0
100
200
300
Time: 181

これはVC ++のバージョンよりも約35%遅く、g ++のバージョンよりも約16%遅くなります。

反復回数を希望の2000に増やすと、差は3%に低下します。この場合のC ++の利点の一部は、実際には実行自体ではなく、単に高速なロード(Javaの永続的な問題)であることを示唆しています。この場合、これは驚くべきことではありません。(ポストされたコードで)測定される計算は非常に簡単なので、ほとんどのコンパイラーが最適化のために多くのことを行うことができるとは思えません。


1
パフォーマンスに大きな影響を与えることはほとんどありませんが、ブール変数をグループ化すること(一般に、同じタイプの変数をグループ化すること)には、まだ改善の余地があります。
ステファン

1
@stefan:ありますが、コードを大幅に最適化することは意図的に避け、代わりに(大まかに)元の実装で最も明白な問題を取り除くために最低限必要なことを行いました。本当に最適化したい場合は#pragma omp、と、(おそらく)少し作業を追加して、各ループの反復が独立していることを確認します。これは、〜Nxのスピードアップを得るのにかなり最小限の作業で済みます。ここで、Nは使用可能なプロセッサコアの数です。
Jerry Coffin

いい視点ね。これは、この質問に対する答えとして十分です
stefan

181時間単位が0.135時間単位より35%遅く、16時間単位が0.156時間単位よりも遅いのはなぜですか?Javaバージョンの期間が0.181であることを意味しましたか?
jamesdlin 2015

1
@jamesdlin:異なる単位を使用しています(元の状態のままだったため、そのままにしました)。C ++コードは時間を秒単位で示しますが、Javaコードは時間をミリ秒単位で示します。
Jerry Coffin、

9

これはメモリの割り当てに関するものだと思います。

Javaプログラムの起動時に大きな連続したブロックをつかむと思いますが、C++、OSにビットと断片を要求します。

この理論をテストするために、C++バージョンに1つの変更を加えたところ、バージョンよりもわずかに速く実行を開始しましたJava

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

事前割り当てベクトルなしのランタイム:

0
100
200
300
Time: 1250.31

事前割り当てベクトルを使用したランタイム:

0
100
200
300
Time: 331.214

Javaバージョンのランタイム:

0
100
200
300
Time: 407

まあ、あなたは本当にそれに依存することはできません。のデータFloodIsolationはまだ他の場所に割り当てられている可能性があります。
stefan 2015

@stefanまだ興味深い結果です。
キリン船長、2015

それは@CaptainGiraffe、私はそれが;-)無用だと言うしませんでした
ステファン・

2
@stefan私はそれを解決策として提案するのではなく、単に私が問題だと思うものを調査するだけです。キャッシングとは何の関係もないようですが、C ++ RTSがJavaとどのように異なるかです。
Galik

1
@Galikそれが常に原因であるとは限りません、それがプラットフォームに大きな影響を与えるのを見るのはかなり興味深いです。ideoneでは、結果を再現できません(割り当てられたブロックは再利用されていないようです)。 ideone.com/im4NMOはしかし、構造体液のベクトルは、より一貫したパフォーマンスへの影響を持っていますideone.com/b0VWSN
ステファンを
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.