Java使用数组的速度是C ++中std :: vector的8倍。我做错什么了?


88

我有以下带有几个大数组的Java代码,这些数组永远不会改变其大小。它在我的计算机上运行1100毫秒。

我在C ++中实现了相同的代码并使用了std::vector

在我的计算机上,运行完全相同的代码的C ++实现的时间为8800 ms。我做错了什么,所以运行缓慢?

基本上,代码执行以下操作:

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上,我只能运行400次而不是2000次循环。但是即使在这里,相差三倍)


42
std::vector<bool>每个元素使用一位来节省空间,这会导致很多移位。如果要提高速度,则应该远离它。使用std::vector<int>代替。
molbdnilo

44
@molbdnilo或std :: vector <char>。有没有必要浪费多;-)
斯特凡

7
很有趣。当单元数为200时,c ++版本会更快。缓存局部性吗?
长颈鹿队长

9
第II部分:最好创建一个单独的类/结构,该类/结构包含数组的每个成员之一,然后具有该结构的单个对象数组,因为实际上您只在内存中循环一次,因此一个方向。
Timo Geusch 2015年

9
@TimoGeusch:尽管我认为h[i] += 1;或(更好)++h[i]比可读性强h[i] = h[i] + 1;,但看到它们之间的速度有任何显着差异,我会有些惊讶。编译器可以“弄清楚”他们都在做相同的事情,并以任何一种方式生成相同的代码(至少在大多数情况下)。
杰里·科芬

Answers:


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比1631)。

奇怪的是,JIT注意到缓存烧毁了整个地方的访问数据,并将您的代码转换为逻辑上相似但效率更高的顺序。

我还关闭了stdio同步,这仅在将printf/ scanf与C ++ std::cout和混合使用时才需要std::cin。碰巧的是,您只打印了一些值,但是C ++的默认打印行为过于偏执且效率低下。

如果nEdges不是实际的常数值,则必须从中删除3个“数组”值struct。那不应该造成巨大的性能损失。

struct通过减小大小对值进行排序,从而减少内存占用量(并在无关紧要的情况下对访问进行排序),可能还可以提高性能。但是我不确定。

一条经验法则是,单个高速缓存未命中的开销比指令高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;
        }
    }

}

这在一定程度上表明,高速缓存未命中是速度下降的原因。同样重要的是要注意,变量不相关,因此可以轻松创建线程解决方案。

订单已恢复

根据Stefans的评论,我尝试使用原始大小将它们分组在结构中。这以类似的方式消除了立即的缓存压力。结果是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
将数据分组到一个结构中并且只有一个向量可能是一个好主意
stefan 2015年

好吧,我在移动设备上,所以我无法进行测量;-),但是一个向量应该很好(也在分配方面)
stefan 2015年

1
是否++以任何身份使用帮助?x = x + 1++x。相比似乎笨拙的。
tadman

3
请更正拼写错误的单词“结果”。:
。-)

1
如果整个迭代器都适合单个寄存器,那么在某些情况下进行复制实际上比在原处进行更新要快。如果您要进行适当的更新,这是因为您很可能之后才使用更新的值。因此,您具有先写后读的依赖关系。如果更新但仅需要旧值,则这些操作不会相互依赖,并且CPU有更多空间并行执行它们(例如,在不同的管道上),从而提高了有效IPC。
PiotrKołaczkowski15年

20

正如@Stefan在对@CaptainGiraffe的答案的评论中所猜到的那样,通过使用结构的向量而不是向量的结构,您可以获得很多收益。更正后的代码如下所示:

#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
仍有改进的空间,尽管这很可能不会显着影响性能:将布尔变量分组(通常将相同类型的变量分组)。
stefan

1
@stefan:有,但是我有意避免对代码进行任何大的优化,而是(粗略地)执行消除原始实现中最明显问题所需的最少工作。如果我真的想优化,我将添加#pragma omp,(也许)做一些工作以确保每个循环迭代都是独立的。要达到〜Nx的加速速度,将花费最少的工作,其中N是可用处理器内核的数量。
杰里·科芬

好点子。这足以回答这个问题
stefan 2015年

181个时间单位如何比0.135个时间单位慢35%,比0.156个时间单位慢16%?您是说Java版本的有效期是0.181吗?
jamesdlin

1
@jamesdlin:他们使用的是不同的单位(以这种方式保留,因为原来是这样的)。C ++代码以秒为单位给出时间,而Java代码以毫秒为单位给出时间。
杰里·科芬

9

我怀疑这与内存分配有关。

我认为这会Java在程序启动时抓住一个大的连续块,而C++在运行过程中要求操作系统提供点点滴滴。

为了验证该理论,我对C++版本进行了一次修改,然后突然开始比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

@stefan仍然是一个有趣的结果。
长颈鹿队长

@CaptainGiraffe,是的,我没有说这没用;-)
stefan 2015年

2
@stefan我并没有提议将其作为解决方案,而只是研究我认为是问题所在。似乎与缓存没有任何关系,但是C ++ RTS与Java有何不同。
Galik 2015年

1
@Galik不一定总是起因,尽管看到它对您的平台产生如此大的影响是很有趣的。在ideone上,我无法重现您的结果(看来,已分配的块不会被重用):ideone.com/im4NMO但是,结构解决方案的向量对性能的影响更为一致:ideone.com/b0VWSN
stefan
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.