web-dev-qa-db-fra.com

Quelle est la vitesse de D par rapport à C ++?

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:

  • 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 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;
}
127
Lars

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.

62
Vladimir Panteleev

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.

31
dsimcha

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.

28

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.

Questions pour l'OP

Est-il intentionnel d'utiliser la même graine pour chaque itération de C++, mais pas pour D?

Installer

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

  • Modification des importations pour être sélective
  • Opérateur de pow d'occasion (^^) au lieu de la multiplication manuelle pour la dernière étape du calcul de la moyenne
  • Enlevé le size_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:

  • C++: clang++ scalar.cpp -o"scalar.cpp.exe" -std=c++11 -O3
  • RÉ: 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:

  1. scalar.cpp (C++ d'origine):

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

    C++ définit 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 fonctionné pendant ~ 2957 ms . Plus lent que l'implémentation C++, mais pas trop.

  3. 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.

  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 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.

13
Erich Gubler

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 ...".

8
Trass3r

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.

7
Jonathan M Davis

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

  • Quel compilateur vous utilisez
  • Quelle fonctionnalité vous utilisez
  • comment vous optimisez agressivement

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.

4
BCS

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 ....)

3
GManNickG