J'aime certaines fonctionnalités de D, mais serais-je intéressé si elles s'accompagnaient d'une pénalité d'exécution?
Pour comparer, j'ai implémenté un programme simple qui calcule les produits scalaires de nombreux vecteurs courts à la fois en C++ et en D. Le résultat est surprenant:
Le C++ est-il vraiment presque cinq fois plus rapide ou ai-je fait une erreur dans le programme D?
J'ai compilé C++ avec g ++ -O3 (gcc-snapshot 2011-02-19) et D avec dmd -O (dmd 2.052) sur un bureau Linux récent modéré. Les résultats sont reproductibles sur plusieurs séries et les écarts-types sont négligeables.
Voici le programme 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;
}
Et voici la version 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;
}
Pour activer toutes les optimisations et désactiver tous les contrôles de sécurité, compilez votre programme D avec les indicateurs DMD suivants:
-O -inline -release -noboundscheck
EDIT : J'ai essayé vos programmes avec g ++, dmd et gdc. dmd est en retard, mais gdc atteint des performances très proches de g ++. La ligne de commande que j'ai utilisée était gdmd -O -release -inline
(gdmd est un wrapper autour de gdc qui accepte les options dmd).
En regardant la liste des assembleurs, il ne semble ni dmd ni gdc inline scalar_product
, mais g ++/gdc a émis des instructions MMX, donc elles pourraient auto-vectoriser la boucle.
Une grande chose qui ralentit D est une implémentation de récupération de place inférieure à la moyenne. Les références qui ne mettent pas beaucoup l'accent sur le GC afficheront des performances très similaires au code C et C++ compilé avec le même backend de compilateur. Les repères qui mettent fortement l'accent sur le GC montreront que D fonctionne de manière abyssale. Rassurez-vous, cependant, il s'agit d'un problème de qualité de mise en œuvre unique (bien que grave), et non d'une garantie de lenteur. En outre, D vous donne la possibilité de désactiver le GC et d'ajuster la gestion de la mémoire dans les bits critiques pour les performances, tout en l'utilisant dans les 95% les moins critiques de votre code.
J'ai j'ai fait des efforts pour améliorer les performances du GC récemment et les résultats ont été plutôt spectaculaires, du moins sur les benchmarks synthétiques. Espérons que ces changements seront intégrés dans l'une des prochaines versions et atténueront le problème.
C'est un fil très instructif, merci pour tout le travail au PO et aux aides.
Une remarque - ce test n'évalue pas la question générale de l'abstraction/pénalité de fonctionnalité ou même celle de la qualité du backend. Il se concentre sur pratiquement une optimisation (optimisation de boucle). Je pense qu'il est juste de dire que le backend de gcc est un peu plus raffiné que celui de dmd, mais ce serait une erreur de supposer que l'écart entre eux est aussi grand pour toutes les tâches.
Semble définitivement comme un problème de qualité de mise en œuvre.
J'ai fait quelques tests avec le code OP et j'ai fait quelques changements. En fait, j'ai accéléré D pour LDC/clang ++, en supposant que les tableaux doivent être alloués dynamiquement (xs
et scalaires associés). Voir ci-dessous pour quelques chiffres.
Est-il intentionnel d'utiliser la même graine pour chaque itération de C++, mais pas pour D?
J'ai modifié la source D d'origine (doublée scalar.d
) pour le rendre portable entre les plates-formes. Cela impliquait uniquement de changer le type des numéros utilisés pour accéder et modifier la taille des tableaux.
Après cela, j'ai apporté les modifications suivantes:
uninitializedArray
a été utilisé pour éviter les inits par défaut des scalaires dans xs (probablement la plus grande différence). Ceci est important parce que D insère normalement tout par défaut, ce que C++ ne fait pas.
Supprimé le code d'impression et remplacé writefln
par writeln
^^
) au lieu de la multiplication manuelle pour la dernière étape du calcul de la moyennesize_type
et remplacé de manière appropriée par le nouveau index_type
alias... résultant ainsi 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;
}
Après avoir testé scalar2.d
(qui priorisait l'optimisation pour la vitesse), par curiosité, j'ai remplacé les boucles dans main
par foreach
équivalents, et je l'ai appelé scalar3.d
( 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
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;
}
J'ai compilé chacun de ces tests à l'aide d'un compilateur basé sur LLVM, car LDC semble être la meilleure option pour la compilation D en termes de performances. Sur mon installation x86_64 Arch Linux, j'ai utilisé les packages suivants:
clang 3.6.0-3
ldc 1:0.15.1-4
dtools 2.067.0-2
J'ai utilisé les commandes suivantes pour compiler chacune:
clang++ scalar.cpp -o"scalar.cpp.exe" -std=c++11 -O3
rdmd --compiler=ldc2 -O3 -boundscheck=off <sourcefile>
Les résultats ( capture d'écran de la sortie brute de la console ) de chaque version de la source comme suit:
scalar.cpp
(C++ d'origine):
allocation: 2 ms
random generation: 12 ms
result: 29248300000
time: 2582 ms
C++ définit la norme à 2582 ms .
scalar.d
(source OP modifiée):
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
Cela a fonctionné pendant ~ 2957 ms . Plus lent que l'implémentation C++, mais pas trop.
scalar2.d
(changement de type d'index/de longueur et optimisation de tableau non initialisé):
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
En d'autres termes, ~ 1860 ms . Jusqu'à présent, c'est en tête.
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 est plus lent que scalar2.d
, mais plus rapide que la version C++.
Avec les optimisations correctes, l'implémentation D est en fait allée plus vite que son implémentation C++ équivalente en utilisant les compilateurs basés sur LLVM disponibles. L'écart actuel entre D et C++ pour la plupart des applications ne semble être basé que sur les limitations des implémentations actuelles.
dmd est l'implémentation de référence du langage et donc la plupart du travail est mis dans le frontend pour corriger les bugs plutôt que d'optimiser le backend.
"in" est plus rapide dans votre cas car vous utilisez des tableaux dynamiques qui sont des types de référence. Avec ref, vous introduisez un autre niveau d'indirection (qui est normalement utilisé pour modifier le tableau lui-même et pas seulement le contenu).
Les vecteurs sont généralement implémentés avec des structures où const ref est parfaitement logique. Voir smallptD vs smallpt pour un exemple du monde réel avec des charges d'opérations vectorielles et de l'aléatoire.
Notez que 64 bits peut également faire la différence. Une fois, j'ai manqué que sur x64, gcc compile du code 64 bits tandis que dmd est toujours par défaut 32 (changera lorsque le code 64 bits mûrira). Il y a eu une accélération remarquable avec "dmd -m64 ...".
Que C++ ou D soit plus rapide dépendra probablement beaucoup de ce que vous faites. Je penserais qu'en comparant du C++ bien écrit au code D bien écrit, ils seraient généralement soit de vitesse similaire, soit C++ serait plus rapide, mais ce que le compilateur particulier parvient à optimiser pourrait avoir un grand effet complètement en dehors du langage lui-même.
Cependant, il y a quelques cas où D a de bonnes chances de battre C++ pour la vitesse. Le principal qui me vient à l'esprit serait le traitement des chaînes. Grâce aux capacités de découpage de tableau de D, les chaînes (et les tableaux en général) peuvent être traités beaucoup plus rapidement que vous ne pouvez le faire facilement en C++. Pour D1, le processeur XML de Tango est extrêmement rapide , grâce principalement aux capacités de découpage de tableau de D (et j'espère que D2 aura un analyseur XML rapide une fois celui en cours d'élaboration pour Phobos terminé). Donc, en fin de compte, si D ou C++ va être plus rapide, cela dépendra beaucoup de ce que vous faites.
Maintenant, je suis surpris que vous voyez une telle différence de vitesse dans ce cas particulier, mais c'est le genre de chose que je m'attendrais à améliorer à mesure que dmd s'améliore. L'utilisation de gdc pourrait donner de meilleurs résultats et serait probablement une comparaison plus étroite du langage lui-même (plutôt que du backend) étant donné qu'il est basé sur gcc. Mais cela ne m'étonnerait pas du tout s'il y a un certain nombre de choses qui pourraient être faites pour accélérer le code généré par dmd. Je ne pense pas qu'il y ait beaucoup de doute que gcc est plus mature que dmd à ce stade. Et les optimisations de code sont l'un des principaux fruits de la maturité du code.
En fin de compte, ce qui importe, c'est la performance de dmd pour votre application particulière, mais je conviens qu'il serait certainement agréable de savoir dans quelle mesure C++ et D se comparent en général. En théorie, ils devraient être à peu près les mêmes, mais cela dépend vraiment de la mise en œuvre. Je pense cependant qu'un ensemble complet de repères serait nécessaire pour vraiment tester dans quelle mesure les deux se comparent actuellement.
Vous pouvez écrire du code C comme D, pour autant que ce soit plus rapide, cela dépendra de beaucoup de choses:
Les différences dans le premier ne sont pas justes à faire glisser. Le second pourrait donner un avantage au C++ car, le cas échéant, il a moins de fonctionnalités lourdes. Le troisième est le plus amusant: le code D est à certains égards plus facile à optimiser car en général il est plus facile à comprendre. Il a également la capacité de faire un grand nombre de programmes génératifs permettant d'écrire des choses comme du code verbeux et répétitif mais rapide sous une forme plus courte.
Cela ressemble à un problème de qualité de mise en œuvre. Par exemple, voici ce que j'ai testé avec:
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);
}
Avec ManualInline
défini, j'obtiens 28 secondes, mais sans j'obtiens 32. Donc, le compilateur n'inclut même pas cette fonction simple, ce que je pense qu'il est clair qu'elle devrait l'être.
(Ma ligne de commande est dmd -O -noboundscheck -inline -release ...
.)