D与C++相比有多快?

Lar*_*ars 130 c++ performance d runtime

我喜欢D的一些功能,但如果它们带来运行时惩罚会有兴趣吗?

为了比较,我实现了一个简单的程序,用C++和D计算许多短向量的标量积.结果令人惊讶:

  • D:18.9秒[最终运行时见下文]
  • C++:3.8秒

C++真的几乎快了五倍,还是我在D程序中犯了错误?

我在一个温和的最近的Linux桌面上用g ++ -O3(gcc-snapshot 2011-02-19)和D和dmd -O(dmd 2.052)编译了C++.结果可在多次运行中重现,标准偏差可忽略不计.

这里的C++程序:

#include <iostream>
#include <random>
#include <chrono>
#include <string>

#include <vector>
#include <array>

typedef std::chrono::duration<long, std::ratio<1, 1000>> millisecs;
template <typename _T>
long time_since(std::chrono::time_point<_T>& time) {
      long tm = std::chrono::duration_cast<millisecs>( std::chrono::system_clock::now() - time).count();
  time = std::chrono::system_clock::now();
  return tm;
}

const long N = 20000;
const int size = 10;

typedef int value_type;
typedef long long result_type;
typedef std::vector<value_type> vector_t;
typedef typename vector_t::size_type size_type;

inline value_type scalar_product(const vector_t& x, const vector_t& y) {
  value_type res = 0;
  size_type siz = x.size();
  for (size_type i = 0; i < siz; ++i)
    res += x[i] * y[i];
  return res;
}

int main() {
  auto tm_before = std::chrono::system_clock::now();

  // 1. allocate and fill randomly many short vectors
  vector_t* xs = new vector_t [N];
  for (int i = 0; i < N; ++i) {
    xs[i] = vector_t(size);
      }
  std::cerr << "allocation: " << time_since(tm_before) << " ms" << std::endl;

  std::mt19937 rnd_engine;
  std::uniform_int_distribution<value_type> runif_gen(-1000, 1000);
  for (int i = 0; i < N; ++i)
    for (int j = 0; j < size; ++j)
      xs[i][j] = runif_gen(rnd_engine);
  std::cerr << "random generation: " << time_since(tm_before) << " ms" << std::endl;

  // 2. compute all pairwise scalar products:
  time_since(tm_before);
  result_type avg = 0;
  for (int i = 0; i < N; ++i)
    for (int j = 0; j < N; ++j) 
      avg += scalar_product(xs[i], xs[j]);
  avg = avg / N*N;
  auto time = time_since(tm_before);
  std::cout << "result: " << avg << std::endl;
  std::cout << "time: " << time << " ms" << std::endl;
}
Run Code Online (Sandbox Code Playgroud)

这里的D版本:

import std.stdio;
import std.datetime;
import std.random;

const long N = 20000;
const int size = 10;

alias int value_type;
alias long result_type;
alias value_type[] vector_t;
alias uint size_type;

value_type scalar_product(const ref vector_t x, const ref vector_t y) {
  value_type res = 0;
  size_type siz = x.length;
  for (size_type i = 0; i < siz; ++i)
    res += x[i] * y[i];
  return res;
}

int main() {   
  auto tm_before = Clock.currTime();

  // 1. allocate and fill randomly many short vectors
  vector_t[] xs;
  xs.length = N;
  for (int i = 0; i < N; ++i) {
    xs[i].length = size;
  }
  writefln("allocation: %i ", (Clock.currTime() - tm_before));
  tm_before = Clock.currTime();

  for (int i = 0; i < N; ++i)
    for (int j = 0; j < size; ++j)
      xs[i][j] = uniform(-1000, 1000);
  writefln("random: %i ", (Clock.currTime() - tm_before));
  tm_before = Clock.currTime();

  // 2. compute all pairwise scalar products:
  result_type avg = cast(result_type) 0;
  for (int i = 0; i < N; ++i)
    for (int j = 0; j < N; ++j) 
      avg += scalar_product(xs[i], xs[j]);
  avg = avg / N*N;
  writefln("result: %d", avg);
  auto time = Clock.currTime() - tm_before;
  writefln("scalar products: %i ", time);

  return 0;
}
Run Code Online (Sandbox Code Playgroud)

Vla*_*eev 64

要启用所有优化并禁用所有安全检查,请使用以下DMD标志编译D程序:

-O -inline -release -noboundscheck
Run Code Online (Sandbox Code Playgroud)

编辑:我用g ++,dmd和gdc尝试过你的程序.dmd确实落后了,但gdc的性能非常接近g ++.我使用的命令行是gdmd -O -release -inline(gdmd是gdc的包装器,接受dmd选项).

看一下汇编程序列表,它看起来既不是dmd也不是gdc内联scalar_product,但是g ++/gdc确实发出了MMX指令,因此它们可能会自动对循环进行矢量化.

  • 你正在失去C++从未有过的功能.大多数语言都没有给你一个选择. (33认同)
  • @Bernard:in-release,除安全功能外,所有代码都关闭边界检查.真正关闭边界检查使用-release和-noboundscheck. (7认同)
  • @Cyber​​Shadow:我们可以将其视为一种调试与发布构建吗? (6认同)
  • @Cyber​​Shadow谢谢!使用这些标志运行时间显着提高 现在D是12.9秒.但仍然运行超过3倍.@Matthieu M.我不介意用慢速运动进行边界检查来测试程序,一旦调试它就让它在没有边界检查的情况下进行计算.(我现在用C++做同样的事.) (5认同)
  • @Cyber​​Shadow:但是如果你删除了安全检查......你不是失去了D的一些重要功能吗? (3认同)
  • 更新了我的测试和结果的答案. (2认同)

dsi*_*cha 32

减慢D的一个重要因素是subpar垃圾收集实现.不会严重压缩GC的基准测试将显示与使用相同编译器后端编译的C和C++代码非常相似的性能.严重压力GC的基准测试将显示D表现糟糕.但请放心,这是一个单一的(尽管很严重的)实施质量问题,而不是慢速的保证.此外,D使您能够选择退出GC并在性能关键位中调整内存管理,同时仍然在性能较低的95%代码中使用它.

我最近在改进GC性能方面做了一些努力,结果相当戏剧化,至少在综合基准测试方面.希望这些更改将集成到接下来的几个版本之一,并将缓解此问题.

  • @GMan:只有当您除以的数字是2的幂时,位移才有效.如果仅在运行时知道该数字,则编译器无法证明这一点,并且测试和分支将比仅使用div指令慢.我的情况很不寻常,因为这个值只在运行时才知道,但我知道在编译时它会有两个幂. (13认同)
  • 请注意,此示例中发布的程序不会在耗时部分进行分配. (7认同)
  • @GMan:是的,如果你在除以的值是在编译时知道的.不,如果该值仅在运行时已知,那就是我进行优化的情况. (3认同)
  • 我注意到您的更改之一是从除法更改为位移位。这不应该是编译器要做的事情吗? (2认同)

And*_*scu 27

这是一个非常有启发性的主题,感谢OP和助手的所有工作.

一个注意事项 - 此测试不是评估抽象/特征惩罚的一般问题,甚至不评估后端质量的问题.它侧重于几乎一个优化(循环优化).我认为可以说gcc的后端比dmd更精致,但假设它们之间的差距对于所有任务来说都是一样大,那就错了.

  • 作为一名在C++上崭露头角的工程师,你是我的英雄.然而,恭敬地,这应该是评论,而不是答案. (11认同)
  • 我完全同意.后面补充说,我主要对数值计算的性能感兴趣,其中循环优化可能是最重要的.您认为哪些其他优化对数值计算很重要?哪些计算会测试它们?我有兴趣补充我的测试并实施一些更多的测试(如果它们大致一样简单).但是evtl.这是另一个问题吗? (4认同)

Eri*_*ler 13

绝对看起来像是一个实施质量问题.

我用OP的代码运行了一些测试并进行了一些更改.实际上,对于LDC/clang ++,D的运行速度更快,假设必须动态分配数组(xs以及相关的标量).请参阅下面的一些数字.

OP的问题

是否有意为C++的每次迭代使用相同的种子,而对于D则不是这样?

建立

我已经调整了原始D源(配音scalar.d),使其可以在平台之间移植.这仅涉及更改用于访问和修改数组大小的数字的类型.

在此之后,我做了以下更改:

  • 用于uninitializedArray避免xs中标量的默认输入(可能是最大的区别).这很重要,因为D通常默认 - 默认地进入所有内容,而C++则没有.

  • 分解打印代码并替换writeflnwriteln

  • 改变进口是有选择性的
  • 使用pow operator(^^)而不是手动乘法来计算平均值的最后一步
  • 删除size_type并使用新index_type别名进行适当替换

...因此导致scalar2.cpp(pastebin):

    import std.stdio : writeln;
    import std.datetime : Clock, Duration;
    import std.array : uninitializedArray;
    import std.random : uniform;

    alias result_type = long;
    alias value_type = int;
    alias vector_t = value_type[];
    alias index_type = typeof(vector_t.init.length);// Make index integrals portable - Linux is ulong, Win8.1 is uint

    immutable long N = 20000;
    immutable int size = 10;

    // Replaced for loops with appropriate foreach versions
    value_type scalar_product(in ref vector_t x, in ref vector_t y) { // "in" is the same as "const" here
      value_type res = 0;
      for(index_type i = 0; i < size; ++i)
        res += x[i] * y[i];
      return res;
    }

    int main() {
      auto tm_before = Clock.currTime;
      auto countElapsed(in string taskName) { // Factor out printing code
        writeln(taskName, ": ", Clock.currTime - tm_before);
        tm_before = Clock.currTime;
      }

      // 1. allocate and fill randomly many short vectors
      vector_t[] xs = uninitializedArray!(vector_t[])(N);// Avoid default inits of inner arrays
      for(index_type i = 0; i < N; ++i)
        xs[i] = uninitializedArray!(vector_t)(size);// Avoid more default inits of values
      countElapsed("allocation");

      for(index_type i = 0; i < N; ++i)
        for(index_type j = 0; j < size; ++j)
          xs[i][j] = uniform(-1000, 1000);
      countElapsed("random");

      // 2. compute all pairwise scalar products:
      result_type avg = 0;
      for(index_type i = 0; i < N; ++i)
        for(index_type j = 0; j < N; ++j)
          avg += scalar_product(xs[i], xs[j]);
      avg /= N ^^ 2;// Replace manual multiplication with pow operator
      writeln("result: ", avg);
      countElapsed("scalar products");

      return 0;
    }
Run Code Online (Sandbox Code Playgroud)

经过测试scalar2.d(优先考虑速度优化),出于好奇,我mainforeach等价物替换了循环,并称之为scalar3.d(pastebin):

    import std.stdio : writeln;
    import std.datetime : Clock, Duration;
    import std.array : uninitializedArray;
    import std.random : uniform;

    alias result_type = long;
    alias value_type = int;
    alias vector_t = value_type[];
    alias index_type = typeof(vector_t.init.length);// Make index integrals portable - Linux is ulong, Win8.1 is uint

    immutable long N = 20000;
    immutable int size = 10;

    // Replaced for loops with appropriate foreach versions
    value_type scalar_product(in ref vector_t x, in ref vector_t y) { // "in" is the same as "const" here
      value_type res = 0;
      for(index_type i = 0; i < size; ++i)
        res += x[i] * y[i];
      return res;
    }

    int main() {
      auto tm_before = Clock.currTime;
      auto countElapsed(in string taskName) { // Factor out printing code
        writeln(taskName, ": ", Clock.currTime - tm_before);
        tm_before = Clock.currTime;
      }

      // 1. allocate and fill randomly many short vectors
      vector_t[] xs = uninitializedArray!(vector_t[])(N);// Avoid default inits of inner arrays
      foreach(ref x; xs)
        x = uninitializedArray!(vector_t)(size);// Avoid more default inits of values
      countElapsed("allocation");

      foreach(ref x; xs)
        foreach(ref val; x)
          val = uniform(-1000, 1000);
      countElapsed("random");

      // 2. compute all pairwise scalar products:
      result_type avg = 0;
      foreach(const ref x; xs)
        foreach(const ref y; xs)
          avg += scalar_product(x, y);
      avg /= N ^^ 2;// Replace manual multiplication with pow operator
      writeln("result: ", avg);
      countElapsed("scalar products");

      return 0;
    }
Run Code Online (Sandbox Code Playgroud)

我使用基于LLVM的编译器编译了每个测试,因为在性能方面,LDC似乎是D编译的最佳选择.在我的x86_64 Arch Linux安装中,我使用了以下软件包:

  • clang 3.6.0-3
  • ldc 1:0.15.1-4
  • dtools 2.067.0-2

我使用以下命令编译每个:

  • C++: clang++ scalar.cpp -o"scalar.cpp.exe" -std=c++11 -O3
  • d: rdmd --compiler=ldc2 -O3 -boundscheck=off <sourcefile>

结果

每个源版本的结果(原始控制台输出的屏幕截图)如下:

  1. scalar.cpp (原C++):

    allocation: 2 ms
    
    random generation: 12 ms
    
    result: 29248300000
    
    time: 2582 ms
    
    Run Code Online (Sandbox Code Playgroud)

    C++将标准设置为2582 ms.

  2. scalar.d (修改后的OP源):

    allocation: 5 ms, 293 ?s, and 5 hnsecs 
    
    random: 10 ms, 866 ?s, and 4 hnsecs 
    
    result: 53237080000
    
    scalar products: 2 secs, 956 ms, 513 ?s, and 7 hnsecs 
    
    Run Code Online (Sandbox Code Playgroud)

    这持续了大约2957毫秒.比C++实现慢,但不是太多.

  3. scalar2.d (索引/长度类型更改和uninitializedArray优化):

    allocation: 2 ms, 464 ?s, and 2 hnsecs
    
    random: 5 ms, 792 ?s, and 6 hnsecs
    
    result: 59
    
    scalar products: 1 sec, 859 ms, 942 ?s, and 9 hnsecs
    
    Run Code Online (Sandbox Code Playgroud)

    换句话说,~1860毫秒.到目前为止,这是领先的.

  4. scalar3.d (foreaches):

    allocation: 2 ms, 911 ?s, and 3 hnsecs
    
    random: 7 ms, 567 ?s, and 8 hnsecs
    
    result: 189
    
    scalar products: 2 secs, 182 ms, and 366 ?s
    
    Run Code Online (Sandbox Code Playgroud)

    ~2182 msscalar2.d比C++版慢,但速度快.

结论

通过正确的优化,D实现实际上比使用可用的基于LLVM的编译器的等效C++实现更快.目前大多数应用程序的D和C++之间的差距似乎只是基于当前实现的限制.


Tra*_*s3r 8

dmd是该语言的参考实现,因此大多数工作都放在前端修复错误而不是优化后端.

"in"在您的情况下更快,因为您正在使用作为引用类型的动态数组.使用ref引入另一级别的间接(通常用于更改数组本身而不仅仅是内容).

向量通常用结构实现,其中const ref非常有意义.有关矢量运算和随机性的实例,请参阅smallptDsmallpt.

请注意,64位也可以有所作为.我曾经错过了x64 gcc编译64位代码,而dmd仍默认为32(当64位codegen成熟时会改变)."dmd -m64 ......"的速度非常快.


Jon*_*vis 7

C++或D是否更快可能高度依赖于您正在做的事情.我认为,在将编写良好的C++与编写良好的D代码进行比较时,它们通常要么具有相似的速度,要么C++会更快,但是特定的编译器设法优化可能会产生很大的影响,除了语言本身.

但是,在某些情况下,D很有可能在速度上击败C++.想到的主要是字符串处理.由于D的数组切片功能,字符串(和一般的数组)可以比在C++中更快地处理.对于D1,Tango的XML处理器非常,主要得益于D的阵列切片功能(并且希望D2一旦完成了当前正在为Phobos工作的那个,它将具有类似的快速XML解析器).因此,最终D或C++是否会更快将取决于你正在做什么.

现在,我惊讶你在这种特殊情况下看到了速度上的这种差异,但是随着dmd的改进,我希望它会有所改善.使用gdc可能会产生更好的结果,并且可能是语言本身(而不是后端)的更接近的比较,因为它是基于gcc的.但是,如果有许多事情可以加速dmd产生​​的代码,那么我一点都不会感到惊讶.我不认为gcc在这一点上比dmd更成熟的问题.代码优化是代码成熟的主要成果之一.

最重要的是,重要的是dmd对你的特定应用程序的表现如何,但我确实同意,知道C++和D在一般情况下的比较肯定会很好.从理论上讲,它们应该基本相同,但它实际上取决于实现.我认为需要一套全面的基准才能真正测试两者目前的比较情况.

  • 是的,如果任何一种语言的输入/输出明显更快,或者两种语言中的纯数学都明显更快,我会感到惊讶,但字符串操作,内存管理和其他一些东西很容易让一种语言闪耀. (4认同)

BCS*_*BCS 5

你可以用 D 编写 C 代码,至于哪个更快,这取决于很多因素:

  • 你用什么编译器
  • 您使用什么功能
  • 您优化的积极程度如何

第一个差异是不公平的。第二个差异可能会给 C++ 带来优势,因为它(如果有的话)具有较少的重要功能。第三个是有趣的:D 代码在某些方面更容易优化,因为通常它更容易理解。它还能够进行大量的生成式编程,允许以更短的形式编写冗长、重复但快速的代码。