D 和 C + + 相比有多快?

我喜欢 D 的一些特性,但是如果它们带有 运行时罚款?

为了进行比较,我实现了一个简单的程序,它可以计算 C + + 和 D 中许多短向量的标量积。结果令人惊讶:

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

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

我用 g + +-O3(gcc-fast 2011-02-19)编译了 C + + ,用 dmd-O (dmd 2.052)编译了 D。结果在几次运行中是可重复的,标准偏差可以忽略不计。

下面是 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;
}
41163 次浏览

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

-O -inline -release -noboundscheck

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

查看汇编程序清单,看起来 dmd 和 gdc 都没有内联 scalar_product,但 g + +/gdc 确实发出了 MMX 指令,因此它们可能正在自动向量化循环。

C + + 或 D 是否更快很大程度上取决于你在做什么。我认为当比较编写良好的 C + + 和编写良好的 D 代码时,它们通常要么具有相似的速度,要么 C + + 会更快,但是特定的编译器设法优化的东西可能除了语言本身之外还有很大的影响。

然而,在 的一些案例中,D 在速度上有很大的机会击败 C + + 。我想到的主要问题是字符串处理。由于 D 的数组切片功能,字符串(和一般的数组)的处理速度比 C + + 快得多。对于 D1,Tango 的 XML 处理器速度非常快,主要得益于 D 的数组切片功能(希望 D2在目前正在为 Phobos 开发的 XML 解析器完成之后,也能有一个类似的快速 XML 解析器)。因此,最终 D 或 C + + 是否会更快将取决于你正在做的事情。

现在,我对 在这种特殊情况下能看到如此大的速度差异感到惊讶,但是随着 dmd 的改进,我希望这种情况能得到改进。使用 gdc 可能会产生更好的结果,而且考虑到它是基于 gcc 的,因此可能会比较语言本身(而不是后端)。但是,如果有许多事情可以做来加速 dmd 生成的代码,我一点也不会感到惊讶。在这一点上,我不认为 gcc 比 dmd 更成熟有什么问题。代码优化是代码成熟的主要成果之一。

归根结底,重要的是 dmd 在您的特定应用程序中的表现如何,但是我确实同意,了解 C + + 和 D 在一般情况下的表现如何肯定是很好的。理论上,它们应该差不多,但实际上取决于实现。然而,我认为需要一套全面的基准来真正测试这两者目前的比较情况。

降低 D 速度的一个重要原因是垃圾收集实现不够好。不过分强调 GC 的基准测试将显示与使用相同编译器后端编译的 C 和 C + + 代码非常相似的性能。严重强调 GC 的基准测试将显示 D 的表现糟糕透顶。尽管如此,请放心,这只是一个单一的(尽管严重的)实现质量问题,而不是缓慢的固有保证。此外,D 还允许您选择退出 GC,并在性能关键位调优内存管理,同时在性能关键程度较低的95% 的代码中使用它。

我使用了 最近投入了一些精力来改进 GC 的性能,结果相当惊人,至少在合成基准上是这样的。希望这些更改将被集成到接下来的几个版本中,并减轻这个问题。

看起来像是一个实现质量问题。例如,下面是我一直在测试的内容:

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秒,但是没有 ManualInline,我只有32秒。所以编译器甚至没有内联这个简单的函数,我认为很明显它应该是内联的。

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

你可以写 C 代码是 D,只要哪个更快,它将取决于很多事情:

  • 使用什么编译器
  • 你使用什么功能
  • 你有多积极优化

前者的差异不容忽视。第二个版本可能会给 C + + 带来优势,因为它拥有更少的重特性。第三个是有趣的: D 代码在某些方面更容易优化,因为通常它更容易理解。此外,它还能够进行大量的生成式编程,允许以较短的形式编写冗长和重复但快速的代码。

这是一个非常有指导意义的线程,感谢 OP 和帮助者所做的所有工作。

注意-这个测试不是评估抽象/特性损失的一般问题,甚至不是评估后端质量的问题。它主要关注一个优化(循环优化)。我认为可以公平地说,gcc 的后端比 dmd 的后端稍微精致一些,但是假设它们之间的差距对于所有任务都一样大是错误的。

Dmd 是该语言的参考实现,因此大部分工作都放在前端来修复 bug,而不是优化后端。

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

向量通常使用 const ref 非常有意义的结构来实现。请参阅 SmallptD很小以获得一个具有向量操作和随机性负载的实际示例。

请注意,64位也可以有所不同。我曾经错过了在 x64上 gcc 编译64位代码,而 dmd 仍然默认为32(当64位代码成熟时会改变)。“ dmd-m64...”有一个显著的加速。

肯定是实施质量的问题。

我用 OP 的代码运行了一些测试并做了一些修改。用于 LDC/clang + + 的 实际上我让 D 开得更快了,基于动态分配数组 必须的(xs和相关标量)的假设进行操作。下面是一些数字。

观察所的问题

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

设置

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

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

  • 使用 uninitializedArray来避免 xs 中标量的默认初始值(可能产生了最大的不同)

  • 分解出打印代码,用 writeln代替 writefln

  • 将导入更改为有选择性的
  • 在计算平均值的最后一步,用幂算子(^^)代替人工乘法
  • 删除了 size_type并适当地替换为新的 index_type别名

因此导致 scalar2.cpp(面糊) :

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

在测试了 scalar2.d(优先考虑速度优化)之后,出于好奇,我用 foreach等价物替换了 main中的循环,并称之为 scalar3.d(面糊) :

    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 的编译器编译了这些测试中的每一个,因为就性能而言,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
    

    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(索引/长度类型更改和 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
    

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

  4. scalar3.d(前额) :

    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
    

    ~ 2182ms scalar2.d慢,但比 C + + 版快。

结论

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