Java, dizilerle C ++ 'da std :: vector'den 8 kat daha hızlıdır. Neyi yanlış yaptım?


88

Boyutlarını asla değiştirmeyen birkaç büyük diziye sahip aşağıdaki Java koduna sahibim. Bilgisayarımda 1100 ms'de çalışıyor.

Aynı kodu C ++ 'da uyguladım ve kullandım std::vector.

Bilgisayarımda aynı kodu çalıştıran C ++ uygulamasının süresi 8800 ms'dir. Bunu yavaş yapması için neyi yanlış yaptım?

Temel olarak kod şunları yapar:

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

Yaklaşık 20000 büyüklüğünde farklı diziler aracılığıyla yinelenir.

Her iki uygulamayı da aşağıdaki bağlantılarda bulabilirsiniz:

(İdealde, döngüyü zaman sınırlaması nedeniyle 2000 yerine sadece 400 kez çalıştırabildim. Ama burada bile üç kez fark var)


42
std::vector<bool>Yer kazanmak için öğe başına bir bit kullanır, bu da çok fazla bit kaydırmaya yol açar. Hız istiyorsanız, ondan uzak durmalısınız. std::vector<int>Bunun yerine kullanın .
molbdnilo

44
@molbdnilo Veya std :: vektör <char>. Bu kadar israf etmeye gerek yok ;-)
Stefan

7
Yeterince komik. Hücre sayısı 200 olduğunda c ++ sürümü daha hızlıdır. Önbellek yeri?
Kaptan Zürafa

9
Kısım II: Dizilerin her bir üyesini içeren ve ardından bu yapının tek bir nesne dizisine sahip olan ayrı bir sınıf / yapı oluşturmak çok daha iyi olur, çünkü o zaman aslında bellekte yalnızca bir kez yineleme yaparsınız. tek yön.
Timo Geusch

9
@TimoGeusch: Sanırım h[i] += 1;ya da (daha da iyisi) ++h[i]daha okunaklı olsa da h[i] = h[i] + 1;, aralarında önemli bir hız farkı görmek beni biraz şaşırttı. Bir derleyici, her ikisinin de aynı şeyi yaptığını "anlayabilir" ve her iki şekilde de aynı kodu üretebilir (en azından çoğu durumda).
Jerry Coffin

Yanıtlar:


36

Düğüm başına verilerin bir yapı içinde toplandığı C ++ sürümü ve kullanılan bu yapının tek bir vektörü:

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

canlı örnek

Zaman artık Java sürümünün 2 katı hızdadır. (846'ya karşı 1631).

Muhtemelen JIT, verilere her yerden erişimin önbelleğe yazıldığını fark etti ve kodunuzu mantıksal olarak benzer ancak daha verimli bir düzene dönüştürdü.

Eğer karıştırmak eğer sadece ihtiyaç vardır gibi ben de, stdion senkronizasyon kapalı printf/ scanfC ++ ile std::coutve std::cin. Olduğu gibi, yalnızca birkaç değeri yazdırırsınız, ancak C ++ 'ın yazdırma için varsayılan davranışı aşırı derecede paranoyaktır ve verimsizdir.

Eğer nEdgesgerçek sabit bir değer değildir, daha sonra 3 "dizi" değerleri dışarı çıkarılabilir gerekecektir struct. Bu, büyük bir performans artışına neden olmamalıdır.

İçindeki değerleri structküçülterek, böylece bellek ayak izini azaltarak (ve önemli olmadığında erişimi sıralayarak) başka bir performans artışı elde edebilirsiniz . Ama emin değilim.

Temel bir kural, tek bir önbellek eksikliğinin bir talimattan 100 kat daha pahalı olmasıdır. Verilerinizi önbellek tutarlılığına sahip olacak şekilde düzenlemek çok değerlidir.

Verileri yeniden düzenlemek structmümkün değilse , yinelemenizi sırayla her kapsayıcı üzerinde olacak şekilde değiştirebilirsiniz.

Bir kenara, Java ve C ++ sürümlerinde bazı ince farklılıklar olduğunu unutmayın. Fark ettiğim şey, Java sürümünün "her kenar için" döngüsünde 3 değişkene sahip olduğu, C ++ sürümünün ise yalnızca 2 değişkene sahip olduğuydu. Başkaları var mı bilmiyorum.


44

Evet, c ++ sürümündeki önbellek bir darbe alıyor. Görünüşe göre JIT bunun üstesinden gelmek için daha donanımlı.

forİsUpdateNeeded () ' deki dış kısmı daha kısa parçacıklara değiştirirseniz. Fark ortadan kalkar.

Aşağıdaki örnek 4x hızlanma oluşturur.

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

}

Bu, yavaşlamanın nedeninin önbellek kayıpları olduğunu makul bir dereceye kadar gösterir. Değişkenlerin bağımlı olmadıklarına dikkat etmek de önemlidir, bu nedenle iş parçacıklı bir çözüm kolayca oluşturulur.

Sipariş geri yüklendi

Stefan'ın yorumuna göre, onları orijinal boyutları kullanarak bir yapı içinde gruplamayı denedim. Bu, anlık önbellek baskısını benzer bir şekilde ortadan kaldırır. Sonuç, c ++ (CCFLAG -O3) sürümünün java sürümünden yaklaşık% 15 daha hızlı olmasıdır.

Ne kısa ne de güzel değişiyor.

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

Benim sonucum orijinal boyutlar için Jerry Coffins'den biraz farklı. Benim için farklılıklar kalır. Benim java sürümüm 1.7.0_75 olabilir.


12
Bu veriyi bir yapı içinde gruplamak ve sadece bir vektöre sahip olmak iyi bir fikir olabilir
Stefan

Mobil cihazdayım, bu yüzden ölçüm yapamıyorum ;-) ama bir vektör iyi olmalı (aynı zamanda tahsisler açısından)
Stefan

1
++Yardımı herhangi bir kapasitede kullanmak mı ? x = x + 1ile karşılaştırıldığında çok hantal görünüyor ++x.
tadman

3
Lütfen yanlış yazılmış "sonuç" kelimesini düzeltin. Beni öldürüyor .. :)
fleetC0m

1
Yineleyicinin tamamı tek bir kayda sığarsa, o zaman bir kopya oluşturmak, bazı durumlarda yerinde güncellemeden daha hızlı olabilir. Yerinde güncelleme yapıyorsanız, bunun nedeni büyük olasılıkla güncellenmiş değeri hemen sonra kullanıyor olmanızdır. Yani Yazdıktan Sonra Okuma bağımlılığınız var. Güncelleme yapıyorsanız, ancak yalnızca eski değere ihtiyacınız varsa, bu işlemler birbirine bağlı değildir ve CPU'nun bunları paralel olarak yapmak için daha fazla alanı vardır, örneğin farklı ardışık düzenlerde, böylece etkin IPC'yi artırır.
Piotr Kołaczkowski

20

@Stefan'ın @ CaptainGiraffe'nin cevabına yaptığı bir yorumda tahmin ettiği gibi, bir vektör yapısı yerine bir yapı vektörü kullanarak epey bir kazanç elde edersiniz. Düzeltilmiş kod şuna benzer:

#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'den derleyici ile derlendiğinde, aşağıdaki -EHsc -O2b2 -GL -Qpargibi sonuçlar alıyorum:

0
100
200
300
Time: 0.135

G ++ ile derlemek, biraz daha yavaş bir sonuç verir:

0
100
200
300
Time: 0.156

Aynı donanımda, Java 8u45'ten derleyiciyi / JVM'yi kullanarak aşağıdaki gibi sonuçlar alıyorum:

0
100
200
300
Time: 181

Bu, VC ++ sürümünden yaklaşık% 35, g ++ sürümünden yaklaşık% 16 daha yavaştır.

Yineleme sayısını istenen 2000'e yükseltirsek, fark yalnızca% 3'e düşer, bu da C ++ 'nın avantajının bu kısmının, uygulamanın kendisinde değil, yalnızca daha hızlı yükleme (Java ile uzun süreli bir sorun) olduğunu düşündürür. Bu bana bu durumda şaşırtıcı gelmiyor - ölçülen hesaplama (gönderilen kodda) o kadar önemsiz ki çoğu derleyicinin bunu optimize etmek için çok şey yapabileceğinden şüpheliyim.


1
Büyük olasılıkla performansı önemli ölçüde etkilemeyecek olsa da iyileştirme için hala yer var: Boole değişkenlerini gruplamak (genel olarak aynı türdeki değişkenleri gruplamak).
stefan

1
@stefan: Var, ama kasıtlı olarak kodun herhangi bir ağır optimizasyonunu yapmaktan kaçınıyordum ve bunun yerine (kabaca) orijinal uygulamadaki en bariz sorunları ortadan kaldırmak için gereken minimum şeyi yapıyordum. Gerçekten optimize etmek isteseydim, #pragma ompher döngü yinelemesinin bağımsız olmasını sağlamak için bir ve (belki) biraz çalışma eklerdim. Bu, ~ Nx hızlandırma elde etmek için oldukça az çalışma gerektirir; burada N, mevcut işlemci çekirdeklerinin sayısıdır.
Jerry Coffin

İyi bir nokta. Bu, bu sorunun cevabı için yeterli
Stefan

Nasıl 181 zaman birimi 0.135 zaman birimlerinden% 35, 0.156 zaman birimlerinden% 16 daha yavaş? Java sürümünün süresinin 0.181 olduğunu mu söylediniz?
jamesdlin

1
@jamesdlin: farklı birimler kullanıyorlar (bu şekilde bırakıldı, çünkü orijinalde işler böyle idi). C ++ kodu zamanı saniye cinsinden verir, ancak Java kodu zamanı milisaniye cinsinden verir.
Jerry Coffin

9

Bunun bellek tahsisi ile ilgili olduğundan şüpheleniyorum.

Bunu düşünüyorum Javaoysa program başlangıcında geniş bitişik bloğu alır C++olarak başlar ve giderek kırıntılarla için OS sorar.

Bu teoriyi teste tabi tutmak için C++sürümde bir değişiklik yaptım ve birden sürümden biraz daha hızlı çalışmaya başladı 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";
}

Ön ayırma vektörü olmadan çalışma zamanı :

0
100
200
300
Time: 1250.31

Ön ayırma vektörü ile çalışma zamanı :

0
100
200
300
Time: 331.214

JavaSürüm için çalışma zamanı :

0
100
200
300
Time: 407

Buna gerçekten güvenemezsin. Veri FloodIsolationyine de başka bir yere tahsis edilebilir.
Stefan

@stefan Hala ilginç bir sonuç.
Kaptan Zürafa

@CaptainGiraffe, işe yaramaz demedim ;-)
Stefan

2
@stefan Bunu bir çözüm olarak önermiyorum, sadece sorunun ne olduğunu düşündüğüm şeyi araştırıyorum. Önbelleğe alma ile ilgisi olmayabilir, ancak C ++ RTS'nin Java'dan farkı.
Galik

1
@Galik Her zaman neden bu değildir , ancak platformunuz üzerinde büyük bir etkiye sahip olduğunu görmek oldukça ilginçtir. İdeone üzerinde sonucunuzu yeniden üretemiyorum (göründüğü gibi, tahsis edilen blok yeniden kullanılmıyor): ideone.com/im4NMO Ancak, yapı çözümünün vektörü daha tutarlı bir performans etkisine sahiptir: ideone.com/b0VWSN
stefan
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.