J'aime certaines fonctionnalités de D, mais serais-je intéressé si elles venaient 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 à la fois en C ++ et en D. Le résultat est surprenant:
- D: 18,9 s [voir ci-dessous pour l'exécution finale]
- C ++: 3,8 s
Le C ++ est-il vraiment presque cinq fois plus rapide ou ai-je commis 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 et modéré. Les résultats sont reproductibles sur plusieurs essais 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;
}
c++
performance
runtime
d
Lars
la source
la source
avg = avg / N*N
(ordre des opérations).dmd ... trace.def
que j'obtiens unerror: unrecognized file extension def
. Et la documentation dmd pour optlink ne mentionne que Windows.Réponses:
Pour activer toutes les optimisations et désactiver tous les contrôles de sécurité, compilez votre programme D avec les indicateurs DMD suivants:
EDIT : J'ai essayé vos programmes avec g ++, dmd et gdc. dmd est à la traîne, 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 de l'assembleur, il semble que ni dmd ni gdc ne soient intégrés
scalar_product
, mais g ++ / gdc a émis des instructions MMX, donc ils pourraient vectoriser automatiquement la boucle.la source
Une grande chose qui ralentit D est une implémentation de garbage collection inférieure à la moyenne. Les benchmarks qui ne stressent pas fortement le GC afficheront des performances très similaires à celles du code C et C ++ compilé avec le même backend du compilateur. Les repères qui insistent fortement sur le GC montreront que D fonctionne de manière épouvantable. Soyez assuré, cependant, qu'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% moins critiques de votre code.
Ces derniers temps, j'ai déployé des efforts pour améliorer les performances du GC et les résultats ont été plutôt spectaculaires, du moins sur des benchmarks synthétiques. Espérons que ces changements seront intégrés dans l'une des prochaines versions et atténueront le problème.
la source
C'est un fil très instructif, merci pour tout le travail au PO et aux aides.
Une note - ce test n'évalue pas la question générale de l'abstraction / pénalité des fonctionnalités 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.
la source
Cela semble définitivement être un problème de qualité de mise en œuvre.
J'ai effectué quelques tests avec le code de l'OP et apporté quelques modifications. En fait, j'ai accéléré D pour LDC / clang ++, en partant du principe que les tableaux doivent être alloués dynamiquement (
xs
et les scalaires associés). Voir ci-dessous pour quelques chiffres.Questions pour le PO
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 (doublée
scalar.d
) pour la rendre portable entre les plates-formes. Cela impliquait uniquement de changer le type des nombres 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 (cela a probablement fait la plus grande différence). Ceci est important parce que D installe normalement tout par défaut en silence, ce que C ++ ne fait pas.Code d'impression factorisé et remplacé
writefln
parwriteln
^^
) 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 nouvelindex_type
alias... résultant ainsi en
scalar2.cpp
( pastebin ):Après les tests
scalar2.d
(qui ont donné la priorité à l'optimisation de la vitesse), par curiosité, j'ai remplacé les bouclesmain
par desforeach
équivalents et l' ai appeléscalar3.d
( pastebin ):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 Linux Arch x86_64, 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 chacun:
clang++ scalar.cpp -o"scalar.cpp.exe" -std=c++11 -O3
rdmd --compiler=ldc2 -O3 -boundscheck=off <sourcefile>
Résultats
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):C ++ définit la norme à 2582 ms .
scalar.d
(source OP modifiée):Cela a duré ~ 2957 ms . Plus lent que l'implémentation C ++, mais pas trop.
scalar2.d
(changement de type d'index / longueur et optimisation de tableau non initialisé):En d'autres termes, ~ 1860 ms . Jusqu'à présent, c'est en tête.
scalar3.d
(foreaches):~ 2182 ms est plus lent
scalar2.d
que la version C ++, mais plus rapide.Conclusion
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 LLVM disponibles. L'écart actuel entre D et C ++ pour la plupart des applications semble être uniquement basé sur les limitations des implémentations actuelles.
la source
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 bogues plutôt que pour 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 présentant de nombreuses opérations vectorielles et le caractère aléatoire.
Notez que 64 bits peut également faire la différence. J'ai manqué une fois que sur x64, gcc compile le code 64 bits tandis que dmd est toujours par défaut à 32 (changera lorsque le codegen 64 bits mûrit). Il y avait une accélération remarquable avec "dmd -m64 ...".
la source
Le fait que C ++ ou D soit plus rapide dépendra probablement fortement de ce que vous faites. Je pense qu'en comparant du C ++ bien écrit à du 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 effet important en dehors du langage lui-même.
Cependant, il existe 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 des tableaux de D, les chaînes (et les tableaux en général) peuvent être traitées beaucoup plus rapidement que vous ne pouvez le faire facilement en C ++. Pour D1, le processeur XML de Tango est extrêmement rapide , principalement grâce aux capacités de découpage de tableau de D (et j'espère que D2 aura un analyseur XML aussi rapide une fois celui sur lequel on travaille actuellement pour Phobos). Donc, en fin de compte, que D ou C ++ soit plus rapide dépendra beaucoup de ce que vous faites.
Maintenant, je suis surpris que vous voyiez 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 constituerait 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 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 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 performance de dmd pour votre application particulière, mais je suis d'accord qu'il serait certainement bien 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 de points de repère serait toutefois nécessaire pour vraiment tester la façon dont les deux se comparent actuellement.
la source
Vous pouvez écrire du code C est D donc dans la mesure où ce qui est le 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 à C ++ car il a, le cas échéant, 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. En outre, il a la capacité de faire un grand degré de programmation générative permettant à des choses comme du code verbeux et répétitif mais rapide d'être écrit dans des formes plus courtes.
la source
Cela semble être un problème de qualité de mise en œuvre. Par exemple, voici ce que j'ai testé avec:
Avec
ManualInline
défini, j'obtiens 28 secondes, mais sans 32. Donc, le compilateur n'intègre même pas cette fonction simple, ce qui, à mon avis, devrait l'être.(Ma ligne de commande est
dmd -O -noboundscheck -inline -release ...
.)la source