D与C ++相比有多快?

我喜欢D的一些特性,但是如果它们带来运行时惩罚,会感兴趣吗?

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

  • D:18.9 s [参见下面的最终运行时间]
  • C ++:3.8 s

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

我使用g ++ -O3(gcc-snapshot 2011-02-19)编译了C ++,而在最新的linux桌面上编译了d与dmd -O(dmd 2.052)。 结果是可重现的几个运行和标准偏差可以忽略不计。

这里的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; } 

这里的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; } 

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

 -O -inline -release -noboundscheck 

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

看看汇编程序列表,看起来既没有dmd也没有gdc内联scalar_product ,但是g ++ / gdc确实发出了MMX指令,所以它们可能会自动向量化循环。

减慢D速度的一件大事是一个次级垃圾收集实现。 没有严重强调GC的基准testing将显示与使用相同编译器后端编译的C和C ++代码非常相似的性能。 对GC有严重压力的基准将显示Dperformance糟糕。 不过,请放心,这是一个单一的(尽pipe是严重的)质量问题,而不是一个缓慢的保证。 此外,D还可让您select退出GC并调整性能关键位中的内存pipe理,同时仍将其用于性能不太关键的95%的代码中。

我最近在改进GC性能方面做了一些努力 ,结果是相当戏剧性的,至less在合成基准上。 希望这些变化将被整合到下一个版本之中,并将缓解这个问题。

这是一个非常有启发性的线程,感谢所有对OP和帮助者的工作。

一个注意事项 – 这个testing不是评估抽象/function惩罚的一般问题,也不是评估后端质量。 它着重于几乎一个优化(循环优化)。 我认为可以这么说,gcc的后端比dmd更精致一点,但假设它们之间的差距对于所有任务来说都是相当大的,那将是错误的。

绝对看起来像是一个质量的实现问题。

我用OP的代码运行了一些testing,并做了一些修改。 我实际上得到了D更快的LDC /铿锵+ +,假设数组必须dynamic分配( xs和相关的标量)。 看到下面的一些数字。

OP的问题

是有意为每个C ++迭代使用相同的种子,而不是D?

build立

我调整了原来的D源(称为scalar.d ),使它在平台之间可移植。 这只涉及改变用于访问和修改数组大小的数字的types。

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

  • 使用uninitializedArray来避免xs中的标量默认inits(可能是最大的区别)。 这很重要,因为D通常是默认的,默默地处理所有事情,而C ++则不会。

  • 指明了打印代码,并用writeln代替了writefln

  • 改变import是有select的
  • 使用pow运算符( ^^ )而不是手动乘法来计算平均值的最后一步
  • 删除了size_type并用新的index_type别名进行了适当的replace

…因此导致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; } 

经过testingscalar2.d (优化速度优化)后,出于好奇,我用foreachreplace了main循环,并将其命名为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; } 

我使用基于LLVM的编译器编译了每个testing,因为就性能而言,LDC似乎是D编译的最佳select。 在我的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 

    C ++将标准设置为2582毫秒

  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 

    这跑了约2957毫秒 。 比C ++实现慢,但不是太多。

  3. scalar2.d (索引/长度types更改和未初始化的scalar2.d优化):

     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 

    换句话说, 〜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 

    scalar2.d msscalar2.d慢,但比C ++版本快。

结论

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

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

因为你使用的dynamic数组是引用types,“in”在你的情况下更快。 引用你引入另一个层次的间接(通常用来改变数组本身,而不仅仅是内容)。

向量通常用const ref完成的结构实现。 请参阅smallptD与smallpt,了解一个真实世界的示例,其中包含vector操作和随机性的负载。

请注意,64位也可以有所作为。 我曾经错过了64位gcc编译64位代码,而dmd仍然默认为32(当64位代码成熟时会改变)。 “dmd -m64 …”显着加快了速度。

C ++或D是否更快可能高度依赖于你在做什么。 我认为,把编写良好的C ++与编写良好的D代码进行比较时,他们通常要么具有相似的速度,要么C ++会更快,但是特定编译器能够优化的东西除了语言之外完全可以产生很大的影响本身。

但是,有些情况下,D代表速度超过C ++的好机会。 想到的主要是string处理。 由于D的数组切片function,string(和一般的数组)可以比C ++中快速处理的速度快得多。 对于D1来说, Tango的XML处理器速度非常快 ,主要归功于D的数组切片function(希望D2将有一个类似的快速XMLparsing器,一旦目前正在为Phobos工作已经完成)。 所以,最终D或C ++是否会变得更快将会非常依赖于你在做什么。

现在,我感到惊讶的是,在这种情况下你看到了这样的速度差异,但是随着DMD的提高,我希望能够改善这种情况。 使用gdc可能会产生更好的结果,并且可能会更接近语言本身(而不是后端),因为它是基于gcc的。 但是,如果有很多事情可以加速DMM生成的代码,那么我一点也不会感到意外。 在这一点上我不认为gcc比dmd更成熟。 代码优化是代码成熟的主要成果之一。

最终,重要的是dmd对你的特定应用程序的执行情况如何,但是我确实认为知道C ++和D在一般情况下的比较是非常好的。 从理论上说,它们应该差不多,但实际上还是取决于实施。 我认为需要一套全面的基准来真正testing目前两者的比较情况。

你可以写C代码是D,所以只要哪一个更快,这取决于很多东西:

  • 你用什么编译器
  • 你使用什么function
  • 你有多积极的优化

第一个差异是不公平的,第二个差异可能会给C ++带来好处,因为如果有的话,C ++的重要特征就更less了。 第三个是有趣的:D代码在某些方面更容易优化,因为一般来说它更容易理解。 而且它有能力做很大程度的生成编程,允许冗长和重复的东西,但是快速的代码被写成更短的forms。

似乎是一个实施质量问题。 例如,以下是我一直在testing的内容:

 import std.datetime, std.stdio, std.random; version = ManualInline; immutable N = 20000; immutable Size = 10; alias int value_type; alias long result_type; alias value_type[] vector_type; result_type scalar_product(in vector_type x, in vector_type y) in { assert(x.length == y.length); } body { result_type result = 0; foreach(i; 0 .. x.length) result += x[i] * y[i]; return result; } void main() { auto startTime = Clock.currTime(); // 1. allocate vectors vector_type[] vectors = new vector_type[N]; foreach(ref vec; vectors) vec = new value_type[Size]; auto time = Clock.currTime() - startTime; writefln("allocation: %s ", time); startTime = Clock.currTime(); // 2. randomize vectors foreach(ref vec; vectors) foreach(ref e; vec) e = uniform(-1000, 1000); time = Clock.currTime() - startTime; writefln("random: %s ", time); startTime = Clock.currTime(); // 3. compute all pairwise scalar products result_type avg = 0; foreach(vecA; vectors) foreach(vecB; vectors) { version(ManualInline) { result_type result = 0; foreach(i; 0 .. vecA.length) result += vecA[i] * vecB[i]; avg += result; } else { avg += scalar_product(vecA, vecB); } } avg = avg / (N * N); time = Clock.currTime() - startTime; writefln("scalar products: %s ", time); writefln("result: %s", avg); } 

使用ManualInline定义我得到了28秒,但没有得到32.所以编译器甚至没有内联这个简单的函数,我认为这应该是明确的。

(我的命令行是dmd -O -noboundscheck -inline -release ...