私はDのいくつかの機能が好きですが、実行時のペナルティが付いたら興味がありますか?
比較するために、C++とDの両方で多くの短いベクトルのスカラー積を計算する簡単なプログラムを実装しました。結果は驚くべきものです。
C++は実際にはほぼ5倍の速さですか、それともDプログラムでミスを犯しましたか?
中程度の最近のLinuxデスクトップで、g ++ -O3(gcc-snapshot 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;
}
すべての最適化を有効にし、すべての安全性チェックを無効にするには、次のDMDフラグを使用してDプログラムをコンパイルします。
-O -inline -release -noboundscheck
[〜#〜] edit [〜#〜]:私はあなたのプログラムをg ++、dmd、gdcで試しました。 dmdは遅れを取りますが、gdcはg ++に非常に近いパフォーマンスを達成します。私が使用したコマンドラインはgdmd -O -release -inline
(gdmdは、dmdオプションを受け入れるgdcのラッパーです)。
アセンブラーのリストを見ると、dmdもgdcもインライン化されていないようですscalar_product
、ただしg ++/gdcはMMX命令を発行したため、ループを自動ベクトル化する可能性があります。
Dの速度を落とす大きなことの1つは、サブガベージコレクションの実装です。 GCに大きな負荷をかけないベンチマークは、同じコンパイラバックエンドでコンパイルされたCおよびC++コードと非常に類似したパフォーマンスを示します。 GCに大きな負荷をかけるベンチマークは、Dのパフォーマンスが非常に小さいことを示します。ただし、これは単一の(重大ではありますが)実装品質の問題であり、スローインの焼き付き保証ではありません。また、Dを使用すると、パフォーマンスクリティカルなビットでGCをオプトアウトし、メモリ管理を調整しながら、パフォーマンスクリティカルではないコードの95%で引き続き使用することができます。
最近、GCのパフォーマンスを改善するための努力をしました であり、少なくとも合成ベンチマークでは結果はかなり劇的です。これらの変更が次のいくつかのリリースの1つに統合され、問題が軽減されることを願っています。
これは非常に有益なスレッドです。OPとヘルパーへのすべての作業に感謝します。
1つの注意-このテストは、抽象化/機能のペナルティ、またはバックエンドの品質に関する一般的な問題を評価するものではありません。事実上1つの最適化(ループ最適化)に焦点を当てています。 gccのバックエンドはdmdのバックエンドよりもいくらか洗練されていると言ってもいいと思いますが、それらの間のギャップがすべてのタスクに対して同じ大きさであると仮定するのは間違いです。
間違いなく実装品質の問題のようです。
OPのコードを使用していくつかのテストを実行し、いくつかの変更を加えました。 LDC/clang ++の場合、実際にはDが速くなりました、配列must動的に割り当てられます(xs
および関連するスカラー)。いくつかの数字については以下をご覧ください。
DではなくC++の各反復に同じシードを使用することは意図的ですか?
元のDソース( scalar.d
と呼ばれる)を微調整して、プラットフォーム間で移植できるようにしました。これには、配列のサイズにアクセスして変更するために使用される数値のタイプを変更するだけでした。
この後、次の変更を加えました。
uninitializedArray
を使用して、xsのスカラーのデフォルトの初期化を回避しました(おそらく最大の違いがありました)。これは重要です。Dは通常、すべてをサイレントでデフォルトで初期化しますが、C++ではそうしません。
印刷コードを除外し、writefln
をwriteln
に置き換えました
^^
)を使用しました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.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;
}
LDCがパフォーマンスの観点からDコンパイルに最適なオプションであると思われるため、これらの各テストをLLVMベースのコンパイラを使用してコンパイルしました。 x86_64 Arch Linuxインストールでは、次のパッケージを使用しました。
clang 3.6.0-3
ldc 1:0.15.1-4
dtools 2.067.0-2
次のコマンドを使用してそれぞれをコンパイルしました。
clang++ scalar.cpp -o"scalar.cpp.exe" -std=c++11 -O3
rdmd --compiler=ldc2 -O3 -boundscheck=off <sourcefile>
ソースの各バージョンの結果( 生コンソール出力のスクリーンショット )は次のとおりです。
scalar.cpp
(元のC++):
allocation: 2 ms
random generation: 12 ms
result: 29248300000
time: 2582 ms
C++は標準を2582 msに設定します。
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 msで実行されました。 C++の実装よりも遅いですが、それほど多くありません。
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。これまでのところ、これはリードしています。
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 msはscalar2.d
よりも遅いが、C++バージョンよりも速い。
正しい最適化により、実際にD実装は、利用可能なLLVMベースのコンパイラを使用した同等のC++実装よりも速くなりました。ほとんどのアプリケーションのDとC++の現在のギャップは、現在の実装の制限にのみ基づいているようです。
dmdは言語のリファレンス実装であるため、バックエンドを最適化するのではなく、ほとんどの作業をフロントエンドに入れてバグを修正します。
参照型である動的配列を使用しているため、「in」の方が高速です。 refを使用すると、別のレベルの間接参照を導入します(通常は、内容だけでなく配列自体を変更するために使用されます)。
通常、ベクトルはconst refが完全に意味をなす構造体で実装されます。ベクトル演算の負荷とランダム性を特徴とする実際の例については、 smallptD vs. smallpt を参照してください。
64ビットも違いを生むことに注意してください。私はかつてx64でgccが64ビットコードをコンパイルし、dmdがデフォルトの32のままであることを見逃したことがあります(64ビットcodegenが成熟すると変更されます)。 「dmd -m64 ...」で驚くべき高速化がありました。
C++またはDのどちらが速いかは、何をしているのかに大きく依存する可能性があります。よく書かれたC++をよく書かれたDコードと比較すると、一般に同じ速度か、C++の方が高速になると思いますが、最適化するために特定のコンパイラーが管理することは、言語とは別に大きな影響を与える可能性があります自体。
ただし、areがあります。これは、Dが速度のためにC++を破る可能性が高い場合です。思い浮かぶ主なものは文字列処理です。 Dの配列スライシング機能により、文字列(および一般的な配列)は、C++で簡単に実行できるよりもはるかに高速に処理できます。 D1の場合、 TangoのXMLプロセッサはextremelyfast です。これは主にDの配列スライス機能のおかげです(D2にも同様の機能があります。 Phobosで現在作業中のXMLパーサーが完了すると、高速XMLパーサーになります)。したがって、最終的にDとC++のどちらが速くなるかは、何をするかに大きく依存します。
さて、私amは、この特定のケースでこのような速度の違いが見られることを驚きましたが、それは私が期待する種類のものですdmdが改善されるにつれて改善されます。 gdcを使用すると、より良い結果が得られる可能性があり、gccベースであるため、言語自体(バックエンドではなく)のより近い比較になる可能性があります。しかし、dmdが生成するコードを高速化するためにできることはたくさんありますが、私はまったく驚きません。この時点で、gccがdmdよりも成熟しているという疑問はあまりないと思います。また、コードの最適化は、コードの成熟度の主要な成果の1つです。
最終的に重要なのは、特定のアプリケーションでdmdがどれだけうまく機能するかですが、C++とDの一般的な比較を知っていることは間違いなく素晴らしいことです。理論的には、それらはほとんど同じであるはずですが、実際には実装に依存します。しかし、現在の2つの比較を実際にテストするには、包括的なベンチマークセットが必要になると思います。
CコードはDで記述できるため、どちらが高速であるかは、多くのことに依存します。
最初のものの違いは引きずるのが公平ではありません。2番目のものは、C++に利点があるかもしれません。 3番目は楽しいものです。一般的に理解しやすいため、Dコードはいくつかの点で最適化が容易です。また、冗長な反復的な高速コードのようなものを短い形式で作成できるようにする、大規模な生成プログラミングを実行する機能もあります。
実装品質の問題のようです。たとえば、これは私がテストしてきたものです:
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 ...
。)