Java 使用数组比 C++ 中的 std::vector 快 8 倍。我做错了啥?

Posted

技术标签:

【中文标题】Java 使用数组比 C++ 中的 std::vector 快 8 倍。我做错了啥?【英文标题】:Java 8 times faster with arrays than std::vector in C++. What did I do wrong?Java 使用数组比 C++ 中的 std::vector 快 8 倍。我做错了什么? 【发布时间】:2015-06-21 19:01:53 【问题描述】:

我有以下 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 的不同数组。

您可以在以下链接下找到这两种实现:

Java:https://ideone.com/R8KqjT C++:https://ideone.com/Lu7RpE

(在ideone上,由于时间限制,我只能运行循环400次而不是2000次。但即使在这里也有3次的差异)

【问题讨论】:

std::vector&lt;bool&gt; 每个元素使用一位以节省空间,这会导致大量位移。如果你想要速度,你应该远离它。请改用std::vector&lt;int&gt; @molbdnilo 或 std::vector。没必要浪费太多那个 ;-) 很有趣。 c++版本在cell数为200时速度更快。缓存局部性? 第二部分:你最好创建一个单独的类/结构,其中包含数组的每个成员之一,然后拥有这个结构的一个对象数组,因为那样你就是实际上只在一个方向上迭代内存一次。 @TimoGeusch:虽然我认为h[i] += 1; 或(更好的是)++h[i]h[i] = h[i] + 1; 更具可读性,但看到它们之间的速度有任何显着差异,我会有些惊讶。编译器可以“弄清楚”它们都在做同样的事情,并以任何一种方式生成相同的代码(至少在大多数常见情况下)。 【参考方案1】:

是的,c++ 版本中的缓存需要锤击。似乎 JIT 能够更好地处理这个问题。

如果您将 isUpdateNeeded() 中的外部 for 更改为更短的 sn-ps。差异消失了。

下面的示例产生了 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%左右。

Varning 既不短也不漂亮。

#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。

【讨论】:

将数据分组到一个结构中并且只有一个向量可能是个好主意 好吧,我在移动设备上,所以我无法进行测量 ;-) 但一个向量应该很好(在分配方面也是如此) 使用++ 对任何身份都有帮助吗? x = x + 1++x 相比显得非常笨拙。 请修正拼写错误的单词“result”。它杀了我.. :) 如果整个迭代器适合单个寄存器,那么在某些情况下,制作副本实际上可能比就地更新更快。如果您正在就地进行更新,这是因为您很可能会在之后使用更新的值。所以你有一个写后读的依赖。如果您更新但只需要旧值,则这些操作不会相互依赖,并且 CPU 有更多空间并行执行它们,例如在不同的管道上,增加有效的 IPC。【参考方案2】:

这里是 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";

live example

时间现在是 Java 版本的 2 倍。 (846 对 1631)。

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

我还关闭了 stdio 同步,因为只有在 printf/scanf 与 C++ std::coutstd::cin 混合时才需要。碰巧,您只打印出几个值,但 C++ 的默认打印行为过于偏执且效率低下。

如果nEdges 不是一个实际的常量值,则必须从struct 中去除3 个“数组”值。这不会对性能造成巨大影响。

您可以通过减小大小对struct 中的值进行排序来获得另一个性能提升,从而减少内存占用(以及在无关紧要时对访问进行排序)。但我不确定。

一个经验法则是,单个缓存未命中的成本是一条指令的 100 倍。将数据安排为具有缓存一致性具有很大的价值。

如果将数据重新排列为 struct 不可行,您可以将迭代更改为依次遍历每个容器。

顺便说一句,请注意 Java 和 C++ 版本之间存在一些细微差别。我发现的一个是Java版本在“for each edge”循环中有3个变量,而C++只有2个。我让我的匹配Java。不知道还有没有。

【讨论】:

【参考方案3】:

正如@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 的一个长期问题),而不是真正的执行本身。在这种情况下,这并不让我感到惊讶——被测量的计算(在发布的代码中)是如此微不足道,以至于我怀疑大多数编译器可以做很多事情来优化它。

【讨论】:

仍有改进的余地,尽管这很可能不会显着影响性能:将布尔变量分组(通常将相同类型的变量分组)。 @stefan:有,但我有意避免对代码进行任何重度优化,而是(大致)做最少必要的事情来消除原始实现中最明显的问题。如果我真的想优化,我会添加一个#pragma omp,并且(也许)做一些工作来确保每个循环迭代都是独立的。这将需要相当少的工作来获得 ~Nx 加速,其中 N 是可用处理器内核的数量。 好点。这足以回答这个问题 181 个时间单位比 0.135 个时间单位慢 35%,比 0.156 个时间单位慢 16% 是怎么回事?你的意思是Java版本的持续时间是0.181? @jamesdlin:他们使用不同的单位(离开那个方向,因为原来的情况就是这样)。 C++ 代码以秒为单位给出时间,而 Java 代码以毫秒为单位给出时间。【参考方案4】:

我怀疑这是关于内存分配的。

我认为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 仍然是一个有趣的结果。 @CaptainGiraffe 是,我没说没用 ;-) @stefan 我不建议将其作为解决方案,只是调查我认为的问题所在。似乎它可能与缓存无关,但 C++ RTS 与 Java 有何不同。 @Galik 这不是总是的原因,尽管看到它对您的平台产生如此大的影响是相当有趣的。在 ideone 上,我无法重现您的结果(看起来,分配的块没有被重用):ideone.com/im4NMO 但是,结构体解决方案的向量具有更一致的性能影响:ideone.com/b0VWSN

以上是关于Java 使用数组比 C++ 中的 std::vector 快 8 倍。我做错了啥?的主要内容,如果未能解决你的问题,请参考以下文章

c++中怎么获取数组中元素的个数

mt19937是什么鬼

是否存在字符数组比 Java 中的字符串更好的情况

为什么(我的)Java比C ++快25倍?

在 C++ 中,2D 数组的整体操作是不是比 1D 数组表现得像 2D 数组?

c++ 向量比我的动态数组慢?