C ++と比較してDはどれくらい速いですか?


133

私はDのいくつかの機能が好きですが、ランタイムのペナルティが付いていれば興味がありますか?

比較のために、C ++とDの両方で多くの短いベクトルのスカラー積を計算する単純なプログラムを実装しました。結果は驚くべきものです。

  • D:18.9秒[最終的な実行時間については下記を参照]
  • C ++:3.8秒

C ++は本当に5倍近く高速ですか、それともDプログラムでミスを犯しましたか?

C ++をg ++ -O3(gcc-snapshot 2011-02-19)でコンパイルし、Dをdmd -O(dmd 2.052)で中程度の最近の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;
}

3
ちなみに、プログラムのavg = avg / N*N次の行にバグがあります:(操作の順序)。
ウラジミールパンテレエフ

4
あなたは、アレイ/ベクトル演算を使用してコードを書き換えることを試みることができdigitalmars.com/d/2.0/arrays.html
ミハルMinich

10
より良い比較を行うには、同じコンパイラバックエンドを使用する必要があります。DMDとDMC ++、またはGDCとG ++
he_the_great

1
@Sion Sheevok残念ながら、Linuxではdmdプロファイリングを使用できないようですか?私が間違っている場合には(私を修正してください、私はと言えばdmd ... trace.def、私が得るerror: unrecognized file extension defとのためのDMDドキュメント。optlink言及Windowsのみ。
ラース

1
ああ、それが吐き出す.defファイルを気にすることはありません。タイミングは.logファイル内にあります。「リンカがリンクする順序で関数のリストが含まれています」-optlinkが何かを最適化するのに役立ちますか?また、「加えて、ldは標準の「* .def」ファイルを完全にサポートします。これは、オブジェクトファイルのようにリンカーのコマンドラインで指定できます。」に。
Trass3r

回答:


64

すべての最適化を有効にし、すべての安全性チェックを無効にするには、次の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命令を発行しなかったため、ループが自動ベクトル化されている可能性があります。


3
@Cyber​​Shadow:ただし、安全性チェックを削除すると... Dのいくつかの重要な機能が失われませんか?
Matthieu M.11年

33
C ++にはなかった機能を失っています。ほとんどの言語では選択肢がありません。
ウラジミールパンテレエフ

6
@Cyber​​Shadow:これは一種のデバッグvsリリースビルドと考えることができますか?
フランチェスコ

7
@Bernard:-リリースでは、安全な関数を除くすべてのコードの境界チェックがオフになっています。境界チェックを実際にオフにするには、-releaseと-noboundscheckの両方を使用します。
Michal Minich、2011

5
@Cyber​​Shadowありがとうございます!これらのフラグを使用すると、ランタイムが大幅に向上します。現在、Dは12.9秒です。しかし、それでも3倍以上の時間がかかります。@Matthieu M.スローモーションで境界チェックを使用してプログラムをテストしてもかまいません。デバッグしたら、境界チェックなしで計算を実行してください。(今はC ++でも同じです。)
Lars

32

Dの速度を低下させる大きな要因の1つは、ガベージコレクションの実装が不十分であることです。GCに大きな負荷をかけないベンチマークは、同じコンパイラバックエンドでコンパイルされたCおよびC ++コードと非常に類似したパフォーマンスを示します。GCに大きな負荷をかけるベンチマークは、Dがわずかに実行することを示します。ただし、これは単一の(重大ではありますが)実装品質の問題であり、遅延を完全に保証するものではありません。また、Dを使用すると、GCをオプトアウトし、パフォーマンスが重要なビットでメモリ管理を調整しながら、パフォーマンスの重要性が低いコードの95%でそれを使用できます。

私がしました最近GCのパフォーマンスを向上させることにいくつかの努力を入れて、その結果は、少なくとも合成ベンチマークに、かなり劇的でした。うまくいけば、これらの変更が次のいくつかのリリースの1つに統合され、問題が緩和されるでしょう。


1
あなたの変更の1つが除算からビットシフトへの変更であることに気付きました。それはコンパイラがやるべきことではないでしょうか?
GManNickG 2011

3
@GMan:はい。除算する値がコンパイル時にわかっている場合。いいえ、値が実行時にのみ既知である場合、それは私がその最適化を行った場合でした。
dsimcha

@dsimcha:うーん。私はあなたがそれを作ることを知っているなら、コンパイラもできると思います。実装の品質の問題、またはコンパイラーが証明できないいくつかの条件が満たされている必要があることを私は見逃していますか?(私は今Dを学んでいるので、コンパイラーに関するこれらの小さなことは突然私にとって興味深いものになります。:))
GManNickG

13
@GMan:ビットシフトは、除算する数値が2の累乗である場合にのみ機能します。実行時にのみ数値がわかっている場合、コンパイラーはこれを証明できません。また、div命令を使用するよりもテストと分岐が遅くなります。値は実行時にしかわからないので、私の場合は異常ですが、コンパイル時には2のべき乗になることがわかっています。
dsimcha

7
この例でポストされているプログラムは、時間のかかる部分での割り当てを行わないことに注意してください。
ウラジミールパンテレエフ

27

これは非常に有益なスレッドです。OPとヘルパーのすべての作業に感謝します。

1つのメモ-このテストは、抽象化/機能のペナルティの一般的な問題、またはバックエンドの品質の問題さえも評価していません。実質的に1つの最適化(ループ最適化)に焦点を当てています。gccのバックエンドはdmdのバックエンドより多少洗練されていると言っても差し支えないと思いますが、それらの間のギャップがすべてのタスクで同じくらい大きいと仮定するのは誤りです。


4
同意します。後で追加しますが、私は主に数値計算のパフォーマンスに関心があります。ループの最適化がおそらく最も重要です。他にどのような最適化が数値計算に重要だと思いますか?そして、どの計算でそれらをテストしますか?私のテストを補完し、さらにテストを実装することに興味があります(それらが大体同じくらい単純な場合)。しかし、evtl。これはそれ自体別の質問ですか?
Lars

11
あなたはC ++に夢中なエンジニアとして、あなたは私のヒーローです。ただし、これは回答ではなくコメントである必要があります。
アラン

14

確かに、実装の品質の問題のようです。

OPのコードを使用していくつかのテストを実行し、いくつかの変更を加えました。LDC / clang ++の場合、配列を動的に割り当てなければならないという仮定(および関連するスカラー)を前提として、実際にDを高速化しました。いくつかの数値については以下を参照してください。xs

OPに関する質問

C ++の各反復で同じシードが使用されるのは意図的ですか?Dではそうではありませんか?

セットアップ

元のDソース(吹き替えscalar.d)を微調整して、プラットフォーム間で移植できるようにしました。これは、配列のサイズにアクセスして変更するために使用される数値のタイプを変更するだけでした。

この後、次の変更を加えました。

  • uninitializedArrayxsのスカラーのデフォルトの初期化を回避するために使用されます(おそらく最大の違いがありました)。Dは通常、すべてをサイレントにデフォルトで初期化しますが、C ++ではデフォルトで初期化しないため、これは重要です。

  • 印刷コードを分解して置き換えwriteflnwriteln

  • インポートを選択的に変更
  • ^^平均を計算する最後のステップで手動乗算の代わりに捕虜演算子()を使用
  • を削除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(速度の最適化を優先)の後、不思議なことにループmainforeach同等のものに置き換え、それを呼び出しました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;
    }

LDCはパフォーマンスの点で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 (元の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 msです。これまでのところ、これがリードしています。

  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ミリ秒は、より遅いがscalar2.d、C ++バージョンより速い。

結論

正しい最適化により、D実装は実際には、利用可能なLLVMベースのコンパイラーを使用した同等のC ++実装よりも高速になりました。ほとんどのアプリケーションのDとC ++の間の現在のギャップは、現在の実装の制限にのみ基づいているようです。


8

dmdは言語のリファレンス実装であるため、ほとんどの作業はバックエンドを最適化するのではなく、バグを修正するためにフロントエンドに組み込まれます。

参照型である動的配列を使用しているため、「in」の方が高速です。refを使用すると、別のレベルの間接参照を導入できます(これは通常、内容だけでなく配列自体を変更するために使用されます)。

ベクトルは通常、const refが完全に意味を持つ構造体で実装されます。ベクトル演算とランダム性の負荷を特徴とする実際の例については、smallptDsmallptを参照してください。

64ビットでも違いが生じる可能性があることに注意してください。私はかつてx64でgccが64ビットコードをコンパイルするのを見逃していたのに対し、mddはデフォルトで32に設定されています(64ビットcodegenが成熟すると変更されます)。「dmd -m64 ...」により、驚くべきスピードアップがありました。


7

C ++とDのどちらが速いかは、実行していることに大きく依存している可能性があります。適切に作成されたC ++を適切に作成されたDコードと比較すると、通常は速度が同じか、C ++の方が速いと思いますが、特定のコンパイラーが最適化するために管理するものは、言語とは別に大きな影響を与える可能性があります自体。

ただし、Dが速度のためにC ++を打ち負かす可能性が高い場合がいくつかあります。頭に浮かぶのは、文字列処理です。Dの配列スライシング能力のおかげで、文字列(および一般に配列)は、C ++で簡単に実行できるよりもはるかに高速に処理できます。D1の場合、TangoのXMLプロセッサは非常に高速です主にDの配列スライス機能のおかげで(そして、D2がPhobosで現在作業中のXMLパーサーが完了すると、同様に高速なXMLパーサーになると期待しています)。つまり、最終的にDとC ++のどちらが高速になるかは、実行していることに大きく依存します。

さて、この特定のケースでは速度にこのような違いがあること驚いていますが、これは、mddの改善に伴って改善されると期待することです。gdcを使用すると、gccベースであることを考えると、より良い結果が得られ、言語自体(バックエンドではなく)のより密接な比較になる可能性があります。しかし、dmdが生成するコードを高速化するために実行できることがいくつかあるとしても、私はまったく驚かないでしょう。現時点では、gccがdmdよりも成熟しているという疑問はあまりありません。そして、コードの最適化は、コード成熟度の主要な成果の1つです。

最終的に重要なのは、特定のアプリケーションでdmdがどれだけうまく機能するかですが、C ++とDが一般にどれだけうまく比較できるかを知っておくのは間違いなくいいことだと私は同意します。理論的には、それらはほとんど同じであるはずですが、実際には実装によって異なります。ただし、2つが現在どの程度比較できるかを実際にテストするには、包括的なベンチマークセットが必要になると思います。


4
はい、どちらの言語でも入力/出力が大幅に高速であるか、純粋な数学がどちらかの言語で大幅に高速であるが、文字列操作、メモリ管理、およびその他のいくつかのことにより、1つの言語を簡単に輝かせることができるとは驚きます。
Max Lybbert、2011

1
C ++のiostreamよりも簡単に(高速に)実行できます。しかし、これは主にライブラリ実装の問題です(最も人気のあるベンダーのすべての既知のバージョン)。
Ben Voigt

4

CコードはDで書くことができるので、より高速である限り、多くのことに依存します。

  • 使用するコンパイラ
  • 使用する機能
  • どれだけ積極的に最適化するか

最初のものの違いは、引きずり込むのに公平ではありません。2番目のものは、C ++に利点があるかもしれません。3つ目はおもしろいものです。一般に理解しやすいため、いくつかの点でDコードは最適化が簡単です。また、生成プログラミングを大量に実行する機能も備えており、冗長で反復的だが高速なコードをより短い形式で記述できます。


3

実装の品質の問題のようです。たとえば、これは私がテストしてきたものです:

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 ++タイミングとの比較も行わない限り、タイミングは意味がありません。
減速

3
@ダニエル:あなたは要点を逃しています。それは、Dの最適化を分離して示すことでした。つまり、「コンパイラーはこの単純な関数をインライン化することさえしていません。そうすべきであるのは明らかだと思います。」最初の文で「実装品質の問題のようだ」とはっきり述べたように、私はそれをC ++と比較しようとしています。
GManNickG

ああ本当、ごめんなさい:)。また、DMDコンパイラーもループをベクトル化しないこともわかります。
減速
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.