【问题标题】:How fast is D compared to C++?D 与 C++ 相比有多快?
【发布时间】:2011-07-05 18:27:54
【问题描述】:

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

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

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

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

我使用 g++ -O3 (gcc-snapshot 2011-02-19) 编译了 C++,使用 dmd -O (dmd 2.052) 在最近的 linux 桌面上编译了 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;
}

【问题讨论】:

  • 顺便说一句,您的程序在这一行有一个错误:avg = avg / N*N(操作顺序)。
  • 您可以尝试使用数组/向量操作重写代码digitalmars.com/d/2.0/arrays.html
  • 为了提供更好的比较,您应该使用相同的编译器后端。 DMD 和 DMC++ 或 GDC 和 G++
  • @Sion Sheevok 不幸的是,dmd 分析似乎不适用于 Linux? (如果我错了,请纠正我,但如果我说dmd ... trace.def,我会得到error: unrecognized file extension defoptlink 的 dmd 文档仅提及 Windows。
  • 啊,从不关心它吐出的那个 .def 文件。计时在 .log 文件中。 “它包含链接器应该链接它们的顺序的函数列表” - 也许这有助于 optlink 优化某些东西?另请注意,“此外,ld 完全支持标准的“*.def”文件,这些文件可以像目标文件一样在链接器命令行上指定”-因此,如果您非常需要,可以尝试通过 -L 传递 trace.def到。

标签: c++ performance runtime d


【解决方案1】:

要启用所有优化并禁用所有安全检查,请使用以下 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 指令,因此它们可能正在自动矢量化循环。

【讨论】:

  • @Cyber​​Shadow:但是如果你去掉安全检查......你不会失去 D 的一些重要功能吗?
  • 您正在失去 C++ 从未有过的功能。大多数语言没有给你选择。
  • @Cyber​​Shadow:我们可以将其视为一种调试与发布版本吗?
  • @Bernard:在 -release 中,除安全函数外,所有代码的边界检查都已关闭。要真正关闭边界检查,请同时使用 -release 和-noboundscheck。
  • @Cyber​​Shadow 谢谢!使用这些标志运行时间大大提高。现在 D 为 12.9 秒。但仍然运行 3 倍以上的时间。 @Matthieu M。我不介意以慢动作测试带有边界检查的程序,一旦调试完毕,让它在没有边界检查的情况下进行计算。 (我现在对 C++ 也是如此。)
【解决方案2】:

减慢 D 速度的一件大事是垃圾收集实现低于标准。不会对 GC 造成太大压力的基准测试将显示出与使用相同编译器后端编译的 C 和 C++ 代码非常相似的性能。严重强调 GC 的基准将显示 D 的表现非常糟糕。不过请放心,这是一个单一的(尽管很严重)实施质量问题,而不是对缓慢的根本保证。此外,D 使您能够选择退出 GC 并在性能关键位中调整内存管理,同时仍可在对性能不太重要的 95% 代码中使用它。

我有put some effort into improving GC performance lately,结果相当引人注目,至少在综合基准测试中是这样。希望这些更改能够集成到接下来的几个版本之一中,并缓解该问题。

【讨论】:

  • 我注意到您的更改之一是从除法更改为位移位。那不应该是编译器做的事情吗?
  • @GMan:是的,如果您要除以的值在编译时是已知的。不,如果该值仅在运行时已知,我就进行了优化。
  • @dsimcha:嗯。我想如果你知道怎么做,编译器也可以。实现质量问题,或者我错过了编译器无法证明的某些条件需要满足,但你知道吗? (我现在正在学习D,所以这些关于编译器的小东西突然让我感兴趣了。:))
  • @GMan:移位仅在您要除以的数字是 2 的幂时才有效。如果数字仅在运行时已知,编译器无法证明这一点,并且测试和分支将比仅使用 div 指令慢。我的情况很不寻常,因为该值仅在运行时才知道,但我在编译时知道它将是 2 的幂。
  • 请注意,此示例中发布的程序在耗时部分没有进行分配。
【解决方案3】:

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

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

【讨论】:

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

看起来确实是实施质量问题。

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

OP 的问题

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

设置

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

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

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

  • 分解打印代码并将writefln替换为writeln

  • 将导入更改为选择性
  • 使用 pow 运算符 (^^) 代替手动乘法来计算平均值的最后一步
  • 删除了 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;
    }

在测试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;
    }

我使用基于 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 &lt;sourcefile&gt;

结果

各个版本源码的结果(screenshot of raw console output)如下:

  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
    

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

结论

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

【讨论】:

    【解决方案5】:

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

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

    向量通常用结构来实现,其中 const ref 非常有意义。请参阅 smallptDsmallpt,了解具有大量矢量运算和随机性的真实示例。

    请注意,64 位也可以有所作为。我曾经错过了在 x64 上 gcc 编译 64 位代码,而 dmd 仍然默认为 32(将在 64 位代码生成成熟时更改)。 “dmd -m64 ...”有显着的加速。

    【讨论】:

      【解决方案6】:

      C++ 还是 D 更快可能在很大程度上取决于您在做什么。我认为,在将编写良好的 C++ 与编写良好的 D 代码进行比较时,它们通常要么具有相似的速度,要么 C++ 会更快,但是特定编译器设法优化的内容可能会产生很大的影响,除了语言之外自己。

      但是,在少数情况下,D 很有可能在速度上击败 C++。想到的主要是字符串处理。由于 D 的数组切片功能,字符串(和一般的数组)的处理速度比 C++ 中的处理速度要快得多。对于 D1,Tango's XML processor is extremely fast,主要归功于 D 的数组切片功能(希望 D2 将拥有一个类似快速的 XML 解析器,一旦目前正在为 Phobos 工作的解析器已经完成)。因此,最终是 D 还是 C++ 会变得更快将在很大程度上取决于您在做什么。

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

      归根结底,重要的是 dmd 对您的特定应用程序的性能如何,但我同意知道 C++ 和 D 的总体比较情况肯定会很好。从理论上讲,它们应该几乎相同,但这实际上取决于实现。不过,我认为需要一套全面的基准来真正测试两者目前的比较情况。

      【讨论】:

      • 是的,如果输入/输出在任何一种语言中明显更快,或者纯数学在任何一种语言中都明显更快,我会感到惊讶,但字符串操作、内存管理和其他一些事情很容易让一种语言闪耀。
      • 比 C++ iostreams 做得更好(更快)很容易。但这主要是一个库实现问题(在最流行的供应商的所有已知版本上)。
      【解决方案7】:

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

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

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

      【讨论】:

        【解决方案8】:

        似乎是实施质量问题。例如,这是我一直在测试的:

        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 ...。)

        【讨论】:

        • 你的时间是没有意义的,除非你也给你的 C++ 时间进行比较。
        • @Daniel:你没有抓住重点。这是为了单独演示 D 优化,即我所说的结论:“所以编译器甚至没有内联这个简单的函数,我认为很明显它应该是。”我什至试图将它与 C++ 进行比较,正如我在 first 句子中明确指出的那样:“似乎是实现质量问题。”
        • 啊,真的,对不起:)。您还会发现 DMD 编译器也根本不向量化循环。
        猜你喜欢
        • 1970-01-01
        • 2012-09-28
        • 2011-04-13
        • 1970-01-01
        • 2010-09-13
        • 1970-01-01
        • 1970-01-01
        • 2022-01-03
        相关资源
        最近更新 更多