D语言相对于C++有多快?

138

我喜欢D语言的某些特性,但我想知道它们是否会降低运行速度?

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

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

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

我使用"g++ -O3" (gcc-snapshot 2011-02-19)编译了C++,使用"dmd -O" (dmd 2.052)编译了D,在一台中等配置的Linux桌面电脑上运行。结果在多次运行中都可以复现,标准偏差也很小。

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

4
顺便说一下,你的程序在这行代码上有一个bug:avg = avg / N*N(运算顺序问题)。 - Vladimir Panteleev
5
你可以尝试使用数组/向量操作重写这段代码。http://www.digitalmars.com/d/2.0/arrays.html - Michal Minich
11
为了更好地进行比较,您应该使用相同的编译器后端。可以选择 DMD 和 DMC++ 或 GDC 和 G++。 - he_the_great
2
很遗憾,Sion Sheevok,似乎Linux上没有dmd分析功能?(如果我说“dmd ... trace.def”,我会得到一个“错误:未识别的文件扩展名def”。而且dmd文档中的optlink只提到了Windows。请纠正我如果我错了。) - Lars
2
啊,从来不关心它吐出的.def文件。时间记录在.log文件中。 “它包含函数列表,按照链接器应该链接它们的顺序” - 也许这有助于optlink优化某些东西? 还要注意,“此外,ld完全支持标准的“*.def”文件,可以像对象文件一样在链接器命令行上指定” - 所以如果你非常想要,可以尝试通过-L传递trace.def。 - Trass3r
显示剩余9条评论
8个回答

66

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

-O -inline -release -noboundscheck

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

查看汇编清单,似乎dmd和gdc都没有内联scalar_product,但是g++/gdc却发出了MMX指令,所以它们可能正在自动向量化循环。


4
@CyberShadow:但如果您删除安全检查...您不是失去了 D 的一些重要功能吗? - Matthieu M.
37
你失去了C++从未拥有过的特性。大多数语言不会让你选择。 - Vladimir Panteleev
6
我们可以将这种情况看作是调试版本与发布版本的区别吗? - Francesco
7
在-release版本中,除了安全函数外,所有代码的边界检查都被关闭。如果要真正关闭边界检查,请同时使用-release和-noboundscheck参数。 - Michal Minich
5
@CyberShadow 谢谢!使用这些标志,运行时间显著提高。现在 D 只需要 12.9 秒。但仍然比原来慢了三倍以上。@Matthieu M. 我不介意在慢速模式下测试一个带有边界检查的程序,一旦调试完成后,让它在没有边界检查的情况下进行计算。(我现在也在用 C++ 这样做。) - Lars
显示剩余4条评论

34

影响D语言性能的一个主要因素是垃圾回收机制实现不佳。在不过分强调GC的基准测试中,D语言的性能与使用相同编译器后编译的C和C++代码非常相似。但在对GC的压力较大的基准测试中,D语言的性能表现极差。请注意,这只是一个(尽管严重的)实现质量问题,并不一定意味着它天生就慢。此外,D语言允许您在性能关键点中选择退出GC并调整内存管理,同时仍然可以在不那么关乎性能的95%代码中使用GC。

我最近花了一些心思来提高GC性能,至少在合成基准测试中,取得了显著的成效。希望这些变化能够被整合到接下来的几个版本中,并减轻这个问题。


2
我注意到你的一个更改是从除法变成了位移。这不应该是编译器应该做的吗? - GManNickG
4
是的,如果你在编译时知道被除数的值。否则,如果在运行时才知道该值,就不能进行这种优化。这也是我进行该优化时遇到的情况。 - dsimcha
1
@dsimcha:嗯,我觉得如果你知道如何制作它,编译器也可以。这是实现质量问题,还是我错过了一些条件需要满足,而编译器无法证明,但你知道?(我现在正在学习D语言,所以编译器的这些小细节对我来说突然变得有趣了。 :)) - GManNickG
15
@GMan: 只有在除数为2的幂次方时,位移运算才有效。如果只在运行时知道除数的值,编译器无法证明这一点,而测试和分支会比使用div指令更慢。我的情况很特殊,因为值只在运行时才知道,但我在编译时知道它将是2的某个幂次方。 - dsimcha
啊,好的,“测试和分支会比仅使用div指令慢”,我忘记了这一点。谢谢。 :) - GManNickG
8
请注意,此示例中发布的程序未在耗时部分进行分配。 - Vladimir Panteleev

29

这是一个非常有启发性的帖子,感谢原帖和帮助者所做的努力。

需要注意的是,此测试并未评估抽象/特性惩罚或后端质量等一般问题。它集中在一个优化(循环优化)上。我认为可以说,gcc的后端比dmd更加精细,但是错误地假设它们之间的差距对于所有任务都那么大,那是一个错误。


5
我完全同意。后面补充说,我主要关心数值计算的性能,其中循环优化可能是最重要的优化之一。您认为哪些其他优化对数值计算非常重要?哪些计算可以测试它们?如果它们大致相似,我会有兴趣补充我的测试并实施更多测试。但这可能需要另一个问题吗? - Lars
16
作为一名以C++为主要技能的工程师,你是我心目中的英雄。但是恕我直言,这应该作为评论而不是答案。 - Alan

19

这绝对是一个实现质量的问题。

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

针对OP的问题

C++的每个迭代使用相同的种子是否有意?而D则不是这样的?

设置

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

之后,我进行了以下更改:

  • 使用uninitializedArray避免xs中标量的默认初始化(可能产生最大的差异)。这很重要,因为D通常会静默地默认初始化所有内容,而C++则不会。

  • 提取打印代码,并将writefln替换为writeln

  • 更改导入为有选择性的
  • 使用pow运算符(^^)代替手动乘法来计算平均值的最后一步
  • 移除size_type并适当替换为新的index_type别名

...结果得到了scalar2.cpppastebin):

    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 之后,我出于好奇心将 main 中的循环替换为相应的 foreach ,并将其命名为 scalar3.dpastebin):

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

为了获得最佳的D编译性能,我使用基于LLVM的编译器编译了每个测试。 在我的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 (original C++):

    allocation: 2 ms
    
    random generation: 12 ms
    
    result: 29248300000
    
    time: 2582 ms
    

    C++ sets the standard at 2582 ms.

  2. scalar.d (modified OP source):

    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 
    

    This ran for ~2957 ms. Slower than the C++ implementation, but not too much.

  3. scalar2.d (index/length type change and uninitializedArray optimization):

    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
    

    In other words, ~1860 ms. So far this is in the lead.

  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
    

    ~2182 ms is slower than scalar2.d, but faster than the C++ version.

结论

通过正确的优化,D语言实现比使用基于LLVM的编译器的等效C++实现运行得更快。目前大多数应用程序中D语言与C++之间的差距似乎仅基于当前实现的限制。


10

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

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

向量通常使用结构体实现,其中const ref非常合理。请参见smallptDsmallpt的真实世界示例,其中包含大量矢量操作和随机性。

请注意,64位也可能有所不同。我曾经在x64上错过了这一点,gcc编译出64位代码,而dmd仍然默认为32位(当64位代码生成器成熟时将发生变化)。使用"dmd -m64 ..."有了显著的加速。


8
无论是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的总体比较表现将是非常好的。理论上,它们应该几乎相同,但这真的取决于实现。然而,我认为需要全面的基准测试来真正测试两者的比较表现。

4
如果一门语言在输入/输出或者纯数学计算方面表现得更快,我会感到惊讶,但是某些语言在字符串操作、内存管理和其他方面可能会更加出色。请注意,我的翻译不会改变原文的意思。 - Max Lybbert
1
比C++的iostreams更好(更快)很容易实现。但这主要是一个库实现问题(在所有已知版本中,来自最流行供应商的)。 - Ben Voigt

5

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

  • 您使用的编译器
  • 您使用的功能
  • 您的优化程度

第一个因素的差异是不公平的。第二个因素可能会给 C++ 带来优势,因为如果有的话,它具有较少的重型特性。第三个因素很有趣:D 代码在某些方面更容易优化,因为它通常更易于理解。此外,它具有执行大量生成式编程的能力,使得可以编写繁琐但快速的代码以更简洁的形式呈现。


4
似乎是实现质量问题。例如,这是我一直在测试的内容:
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 ...。)

1
除非您还提供了与C++时间比较的数据,否则您的计时数据是没有意义的。 - deceleratedcaviar
3
@Daniel:你没有抓住重点。这是为了独立展示D语言的优化,尤其是针对我所说的结论:“所以编译器甚至没有将这个简单的函数内联,而我认为它应该被内联。” 我甚至试图将其与C++进行比较,正如我在*第一句话中明确说明的那样:“看起来像是实现质量问题。” - GManNickG
啊,对不起:)。你还会发现DMD编译器也没有对循环进行向量化处理。 - deceleratedcaviar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接