Quelle est la rapidité de D par rapport à C ++?

J’aime certaines fonctionnalités de D, mais serait intéressé si elles viennent avec une pénalité d’exécution?

Pour comparer, j’ai implémenté un programme simple qui calcule les produits scalaires de nombreux vecteurs courts en C ++ et en D. Le résultat est surprenant:

  • D: 18.9 s [voir ci-dessous pour le dernier temps d’exécution]
  • C ++: 3.8 s

Est-ce que C ++ est presque cinq fois plus rapide ou est-ce que je me suis trompé 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 et modéré. Les résultats sont reproductibles sur plusieurs essais et les écarts-types sont négligeables.

Voici le programme C ++:

#include  #include  #include  #include  #include  #include  typedef std::chrono::duration<long, std::ratio> millisecs; template  long time_since(std::chrono::time_point& time) { long tm = std::chrono::duration_cast( 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 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 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 toutes les vérifications de sécurité, comstackz 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 réalise 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, cela ne ressemble ni à dmd ni à gdc inline scalar_product , mais g ++ / gdc émet des instructions MMX, donc ils peuvent être auto-vectorisant la boucle.

Une grande chose qui ralentit D est une implémentation de récupération de place inférieure. Les tests de performances qui ne sollicitent pas trop le GC afficheront des performances très similaires à celles du code C et C ++ compilé avec le même backend de compilateur. Les points de repère qui exercent une forte pression sur le GC montreront que D est très performant. Rassurez-vous, cependant, il s’agit d’un problème de qualité de mise en œuvre unique (quoique grave), et non d’une garantie de lenteur. De plus, D vous permet de désactiver la gestion de la GC et de régler la mémoire dans les bits critiques du sharepoint vue des performances, tout en l’utilisant dans 95% moins critique de votre code.

J’ai déployé des efforts pour améliorer les performances du GC ces derniers temps et les résultats ont été plutôt spectaculaires, du moins sur des points de référence synthétiques. Espérons que ces changements seront intégrés dans l’une des prochaines versions et atténueront le problème.

Ceci est un fil très instructif, merci pour tout le travail au PO et aux assistants.

Une remarque: ce test n’évalue pas la question générale de la pénalité d’abstraction / caractéristique ou même celle de la qualité 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.

Cela semble définitivement être un problème de qualité de la mise en œuvre.

J’ai effectué des tests avec le code de l’OP et apporté quelques modifications. En fait, D est allé plus vite pour LDC / clang ++, en supposant que les tableaux doivent être alloués dynamicment ( xs et scalaires associés). Voir ci-dessous pour certains nombres.

Questions pour l’OP

Est-il intentionnel que la même graine soit utilisée pour chaque itération de C ++, alors que ce n’est pas le cas pour D?

Installer

J’ai modifié la source D originale (nommée scalar.d ) pour la rendre portable entre les plates-formes. Cela impliquait uniquement de modifier 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:

  • Utilisé uninitializedArray pour éviter les inits par défaut pour les scalaires dans xs (probablement la plus grande différence). Ceci est important car D inactive normalement tout ce qui est silencieux, ce que C ++ ne fait pas.

  • Sortie du code d’impression et remplacement de writefln par writeln

  • Les importations modifiées sont sélectives
  • Opérateur de pow utilisé ( ^^ ) au lieu d’une multiplication manuelle pour l’étape finale de calcul de la moyenne
  • Suppression du size_type et remplacement approprié par le nouvel alias index_type

… résultant dans 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 priorise l'optimisation pour la vitesse), par curiosité, j'ai remplacé les boucles main par des équivalents foreach , et les scalar3.d appelées 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 en utilisant 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 paquets suivants:

  • clang 3.6.0-3
  • ldc 1:0.15.1-4
  • dtools 2.067.0-2

J'ai utilisé les commandes suivantes pour comstackr chacun:

  • C ++: clang++ scalar.cpp -o"scalar.cpp.exe" -std=c++11 -O3
  • D: rdmd --comstackr=ldc2 -O3 -boundscheck=off

Résultats

Les résultats ( capture d'écran de la sortie de la console brute ) de chaque version de la source sont les suivants:

  1. scalar.cpp (original C ++):

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

    C ++ établit la norme à 2582 ms .

  2. 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 duré environ 2957 ms . Plus lent que l'implémentation C ++, mais pas trop.

  3. scalar2.d (changement de type index / longueur et optimisation de l'initialisation de l'initialisation):

     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.

  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 est plus lent que scalar2.d , mais plus rapide que la version C ++.

Conclusion

Avec les optimisations correctes, l'implémentation D a été plus rapide 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 reposer que sur les limitations des implémentations actuelles.

dmd est l’implémentation de référence du langage et, par conséquent, la plupart du travail est placé dans l’interface pour corriger les bogues plutôt que d’optimiser le backend.

“in” est plus rapide dans votre cas car vous utilisez des tableaux dynamics 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 réel comportant des charges d’opérations vectorielles et des aléas.

Notez que 64 bits peut également faire la différence. J’ai manqué une fois cela sur x64 gcc comstack le code 64 bits alors que dmd est toujours par défaut à 32 (changera quand le codegen 64 bits mûrit). Il y avait une accélération remarquable avec “dmd -m64 …”.

Que C ++ ou D soit plus rapide dépendra probablement de ce que vous faites. Je pense que lorsque l’on compare un code C ++ bien écrit à un code D bien écrit, ils seraient généralement de vitesse similaire, ou C ++ serait plus rapide, mais ce que le compilateur particulier parvient à optimiser pourrait avoir un effet considérable en dehors du langage lui-même

Cependant, il y a quelques cas où D a de bonnes chances de battre le C ++ pour la vitesse. Le principal qui me vient à l’esprit serait le traitement des chaînes. Grâce à la capacité de découpage du tableau de D, les chaînes (et les tableaux en général) peuvent être traitées beaucoup plus rapidement qu’en C ++. Pour D1, le processeur XML de Tango est extrêmement rapide , principalement grâce aux capacités de découpage de masortingce de D (et heureusement, D2 aura un parsingur XML aussi rapide une fois que celui en cours de développement pour Phobos sera terminé). Donc, si D + C ++ va être plus rapide, cela dépendra beaucoup de ce que vous faites.

Maintenant, je suis surpris que vous constatiez une telle différence de vitesse dans ce cas particulier, mais c’est le genre de chose que je m’attendrais à améliorer avec l’amélioration de dmd. Utiliser 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 me surprendrait pas du tout si un certain nombre de choses pouvaient être faites pour accélérer le code généré par dmd. Je ne pense pas qu’il y ait beaucoup de questions sur le fait 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 compte, c’est la qualité des performances de dmd pour votre application particulière, mais je suis d’accord sur le fait qu’il serait certainement intéressant de savoir à quel point 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 qu’un ensemble complet d’indices de référence serait nécessaire pour tester la comparabilité actuelle des deux.

Vous pouvez écrire le code C est D, pour autant que ce soit plus rapide, cela dépendra de beaucoup de choses:

  • Quel compilateur que vous utilisez
  • Quelle fonctionnalité utilisez-vous?
  • comment vous optimisez agressivement

Les différences dans le premier ne sont pas équitables. Le second pourrait donner un avantage au C ++, car il comporte, si tant est qu’un peu, de lourdes fonctionnalités. Le troisième est amusant: le code D est plus facile à optimiser à certains égards car en général, il est plus facile à comprendre. En outre, il a la capacité de réaliser un grand nombre de programmes génératifs permettant d’écrire des codes comme verbeux et répétitifs, mais plus rapides.

Cela semble être 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 defined, j’obtiens 28 secondes, mais sans 32, le compilateur n’inclut même pas cette fonction simple, ce que je pense que cela devrait être clair.

(Ma ligne de commande est dmd -O -noboundscheck -inline -release ... )