Quanto è veloce D rispetto a C ++?


133

Mi piacciono alcune funzionalità di D, ma sarei interessato se arrivassero con una penalità di runtime?

Per fare un confronto, ho implementato un semplice programma che calcola prodotti scalari di molti vettori brevi sia in C ++ che in D. Il risultato è sorprendente:

  • D: 18,9 s [vedi sotto per il runtime finale]
  • C ++: 3,8 s

Il C ++ è davvero quasi cinque volte più veloce o ho fatto un errore nel programma D?

Ho compilato C ++ con g ++ -O3 (gcc-snapshot 19/02/2011) e D con dmd -O (dmd 2.052) su un desktop Linux recente e moderato. I risultati sono riproducibili su più serie e le deviazioni standard sono trascurabili.

Ecco il programma 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;
}

E qui la versione 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;
}

3
A proposito, il tuo programma ha un bug su questa linea: avg = avg / N*N(ordine delle operazioni).
Vladimir Panteleev,

4
Puoi provare a riscrivere il codice usando le operazioni array / vector digitalmars.com/d/2.0/arrays.html
Michal Minich,

10
Per fornire un confronto migliore, è necessario utilizzare lo stesso back-end del compilatore. DMD e DMC ++ o GDC e G ++
he_the_great

1
@Sion Sheevok Sfortunatamente, la profilazione DMD non sembra essere disponibile per Linux? (per favore correggimi se sbaglio, ma se dico di avernedmd ... trace.def uno error: unrecognized file extension def. E i documenti dmd per optlink menzionano solo Windows.
Lars,

1
Ah, non mi è mai importato di quel file .def che sputa. I tempi sono all'interno del file .log. "Contiene l'elenco delle funzioni nell'ordine in cui il linker dovrebbe collegarle" - forse questo aiuta optlink per ottimizzare qualcosa? Si noti inoltre che "Inoltre, ld supporta pienamente i file" * .def "standard, che possono essere specificati sulla riga di comando del linker come un file oggetto" - in modo da poter provare a passare trace.def tramite -L se si desidera per.
Trass3r

Risposte:


64

Per abilitare tutte le ottimizzazioni e disabilitare tutti i controlli di sicurezza, compila il tuo programma D con i seguenti flag DMD:

-O -inline -release -noboundscheck

EDIT : ho provato i tuoi programmi con g ++, dmd e gdc. dmd è in ritardo, ma gdc raggiunge prestazioni molto vicine a g ++. La linea di comando che ho usato era gdmd -O -release -inline(gdmd è un wrapper per gdc che accetta le opzioni dmd).

Guardando l'elenco degli assemblatori, sembra che né dmd né gdc siano in linea scalar_product, ma g ++ / gdc ha emesso istruzioni MMX, quindi potrebbero auto-vettorizzare il ciclo.


3
@CyberShadow: ma se rimuovi il controllo di sicurezza ... non stai perdendo alcune importanti funzionalità di D?
Matthieu M.,

33
Stai perdendo funzionalità che C ++ non ha mai avuto. Molte lingue non ti danno scelta.
Vladimir Panteleev,

6
@CyberShadow: possiamo considerarlo una sorta di build di debug vs release?
Francesco,

7
@Bernard: in-release, il controllo dei limiti è disattivato per tutto il codice tranne le funzioni sicure. per disattivare veramente il controllo dei limiti usare sia -release che-noboundscheck.
Michal Minich,

5
@CyberShadow Grazie! Con questi flag il tempo di esecuzione migliora notevolmente. Ora D è a 12,9 s. Ma funziona ancora più di 3 volte di più. @Matthieu M. Non mi dispiacerebbe testare un programma con il boundschecking al rallentatore e una volta eseguito il debug lascia che esegua i suoi calcoli senza il boundschecking. (Faccio lo stesso con C ++ ora.)
Lars,

32

Una grande cosa che rallenta D è un'implementazione di garbage collection scadente. I benchmark che non stressano molto il GC mostreranno prestazioni molto simili al codice C e C ++ compilato con lo stesso backend del compilatore. I benchmark che sottolineano fortemente il GC mostreranno che D si comporta in modo abissale. Siate certi, tuttavia, questo è un singolo (sebbene grave) problema di qualità dell'attuazione, non una garanzia di lentezza. Inoltre, D ti dà la possibilità di disattivare GC e ottimizzare la gestione della memoria in bit critici per le prestazioni, mentre lo usi ancora nel 95% meno critico del tuo codice.

Ultimamente ho fatto qualche sforzo per migliorare le prestazioni del GC e i risultati sono stati piuttosto drammatici, almeno sui benchmark sintetici. Si spera che questi cambiamenti saranno integrati in una delle prossime versioni e mitigheranno il problema.


1
Ho notato che uno dei tuoi cambiamenti è stato un passaggio dalla divisione al bit shift. Non dovrebbe essere qualcosa che fa il compilatore?
GManNickG,

3
@GMan: Sì, se il valore che stai dividendo è noto al momento della compilazione. No, se il valore è noto solo in fase di esecuzione, è stato il caso in cui ho effettuato tale ottimizzazione.
dsimcha,

@dsimcha: Hm. Immagino che se sai farlo, anche il compilatore può farlo. Problema di qualità dell'implementazione o mi manca che alcune condizioni debbano essere soddisfatte che il compilatore non può provare, ma lo sai? (Sto imparando D ora, quindi queste piccole cose sul compilatore sono improvvisamente interessanti per me. :))
GManNickG

13
@GMan: lo spostamento dei bit funziona solo se il numero che stai dividendo è una potenza di due. Il compilatore non può provarlo se il numero è noto solo in fase di esecuzione e test e ramificazione sarebbero più lenti rispetto al semplice utilizzo dell'istruzione div. Il mio caso è insolito perché il valore è noto solo in fase di esecuzione, ma so al momento della compilazione che sarà una potenza di due.
dsimcha,

7
Si noti che il programma pubblicato in questo esempio non esegue l'allocazione nella porzione che richiede tempo.
Vladimir Panteleev,

27

Questo è un thread molto istruttivo, grazie per tutto il lavoro svolto all'OP e agli aiutanti.

Una nota: questo test non sta valutando la questione generale della penalità di astrazione / funzionalità o anche quella della qualità del backend. Si concentra praticamente su un'ottimizzazione (ottimizzazione del loop). Penso che sia giusto dire che il backend di gcc è un po 'più raffinato di quello di dmd, ma sarebbe un errore supporre che il divario tra loro sia grande per tutti i compiti.


4
Sono completamente d'accordo. Come aggiunto più avanti, sono principalmente interessato alle prestazioni per calcoli numerici in cui l'ottimizzazione del loop è probabilmente la più importante. Quali altre ottimizzazioni pensi siano importanti per il calcolo numerico? E quali calcoli li testerebbero? Sarei interessato a completare il mio test e implementare altri test (se sono approssimativamente altrettanto semplici). Ma evtl. questa è un'altra domanda a sé stante?
Lars

11
Come ingegnere che ha tagliato i denti al C ++, sei un mio eroe. Rispettosamente, tuttavia, questo dovrebbe essere un commento, non una risposta.
Alan,

14

Sicuramente sembra un problema di qualità dell'attuazione.

Ho eseguito alcuni test con il codice OP e apportato alcune modifiche. In realtà ho fatto D andare più veloce per LDC / clang ++, operando partendo dal presupposto che gli array debbano essere allocati in modo dinamico ( xse scalari associati). Vedi sotto per alcuni numeri.

Domande per l'OP

È intenzionale che lo stesso seme venga usato per ogni iterazione di C ++, mentre non lo è per D?

Impostare

Ho modificato l'origine D originale (soprannominata scalar.d) per renderla portatile tra le piattaforme. Ciò ha comportato solo la modifica del tipo di numeri utilizzati per accedere e modificare la dimensione delle matrici.

Successivamente, ho apportato le seguenti modifiche:

  • Utilizzato uninitializedArrayper evitare init predefiniti per scalari in xs (probabilmente ha fatto la differenza più grande). Questo è importante perché D normalmente di default inserisce tutto in silenzio, cosa che C ++ no.

  • Codice di stampa scomposto e sostituito writeflnconwriteln

  • Le importazioni modificate sono state selettive
  • Operatore pow utilizzato ( ^^) anziché moltiplicazione manuale per il passaggio finale del calcolo della media
  • Rimosso size_typee sostituito in modo appropriato con il nuovo index_typealias

... risultando così 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;
    }

Dopo il test scalar2.d(che ha dato priorità all'ottimizzazione per la velocità), per curiosità ho sostituito i loop maincon foreachequivalenti e l'ho chiamato 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;
    }

Ho compilato ciascuno di questi test usando un compilatore basato su LLVM, poiché LDC sembra essere l'opzione migliore per la compilazione D in termini di prestazioni. Nella mia installazione di Arch Linux x86_64 ho usato i seguenti pacchetti:

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

Ho usato i seguenti comandi per compilare ciascuno:

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

risultati

I risultati ( screenshot dell'output della console non elaborata ) di ciascuna versione del sorgente come segue:

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

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

    C ++ imposta lo standard a 2582 ms .

  2. scalar.d (sorgente OP modificata):

    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 

    Questo ha funzionato per ~ 2957 ms . Più lento dell'implementazione C ++, ma non troppo.

  3. scalar2.d (modifica del tipo di indice / lunghezza e ottimizzazione dell'array non inizializzata):

    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

    In altre parole, ~ 1860 ms . Finora questo è in testa.

  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 è più lento di scalar2.d, ma più veloce della versione C ++.

Conclusione

Con le ottimizzazioni corrette, l'implementazione D è andata effettivamente più veloce della sua equivalente implementazione C ++ utilizzando i compilatori basati su LLVM disponibili. L'attuale divario tra D e C ++ per la maggior parte delle applicazioni sembra basarsi solo sui limiti delle attuali implementazioni.


8

dmd è l'implementazione di riferimento del linguaggio e quindi la maggior parte del lavoro viene inserita nel frontend per correggere i bug anziché ottimizzare il backend.

"in" è più veloce nel tuo caso perché stai usando array dinamici che sono tipi di riferimento. Con ref si introduce un altro livello di riferimento indiretto (che viene normalmente utilizzato per modificare l'array stesso e non solo i contenuti).

I vettori sono generalmente implementati con strutture in cui const ref ha perfettamente senso. Vedi smallptD vs. smallpt per un esempio del mondo reale che presenta un sacco di operazioni vettoriali e casualità.

Si noti che anche 64 bit può fare la differenza. Una volta ho perso che su x64 gcc compila il codice a 64 bit mentre dmd è ancora predefinito a 32 (cambierà quando il codegen a 64 bit matura). C'è stato un notevole aumento di velocità con "dmd -m64 ...".


7

Se C ++ o D è più veloce è probabile che dipenda fortemente da quello che stai facendo. Penserei che quando si confronta C ++ ben scritto con codice D ben scritto, generalmente avrebbero una velocità simile o C ++ sarebbe più veloce, ma ciò che il particolare compilatore riesce a ottimizzare potrebbe avere un grande effetto completamente a parte il linguaggio si.

Tuttavia, ci sono alcuni casi in cui D ha buone probabilità di battere C ++ per la velocità. Quello principale che viene in mente sarebbe l'elaborazione delle stringhe. Grazie all'array di D che suddivide le capabalità, le stringhe (e gli array in generale) possono essere elaborate molto più velocemente di quanto si possa facilmente fare in C ++. Per D1, il processore XML di Tango è estremamente veloce , grazie principalmente alle funzionalità di suddivisione in array di D (e si spera che D2 disponga di un analizzatore XML altrettanto veloce una volta completato quello su cui si sta attualmente lavorando per Phobos). Quindi, in definitiva, se D o C ++ sarà più veloce dipenderà molto da quello che stai facendo.

Ora sono sorpreso che tu stia vedendo una tale differenza di velocità in questo caso particolare, ma è il genere di cose che mi aspetterei di migliorare man mano che dmd migliora. L'uso di gdc potrebbe produrre risultati migliori e probabilmente sarebbe un confronto più stretto del linguaggio stesso (piuttosto che del backend) dato che è basato su gcc. Ma non mi sorprenderebbe affatto se ci fossero un certo numero di cose che potrebbero essere fatte per accelerare il codice generato da DMD. Non penso che ci siano molte domande sul fatto che gcc sia più maturo di dmd a questo punto. E le ottimizzazioni del codice sono uno dei frutti principali della maturità del codice.

In definitiva, ciò che conta è quanto bene dmd funzioni per la tua particolare applicazione, ma sono d'accordo che sarebbe sicuramente bello sapere quanto bene C ++ e D si confrontino in generale. In teoria, dovrebbero essere praticamente gli stessi, ma dipende davvero dall'implementazione. Penso che sarebbe necessario un set completo di parametri di riferimento per testare davvero quanto bene i due attualmente confrontano.


4
Sì, sarei sorpreso se l'input / output fosse significativamente più veloce in entrambe le lingue, o se la matematica pura fosse significativamente più veloce in entrambe le lingue, ma le operazioni sulle stringhe, la gestione della memoria e alcune altre cose potrebbero facilmente far brillare una lingua.
Max Lybbert,

1
È facile fare meglio (più velocemente) degli iostreams C ++. Ma questo è principalmente un problema di implementazione della libreria (su tutte le versioni conosciute dei fornitori più popolari).
Ben Voigt,

4

Puoi scrivere il codice C è D, per quanto è più veloce, dipenderà da molte cose:

  • Quale compilatore usi
  • Quale caratteristica usi
  • quanto aggressivamente ottimizzi

Le differenze nel primo non sono corrette da trascinare. Il secondo potrebbe dare un vantaggio al C ++ in quanto, se non altro, ha meno funzioni pesanti. Il terzo è quello divertente: il codice D in qualche modo è più facile da ottimizzare perché in generale è più facile da capire. Inoltre ha la capacità di fare un ampio grado di programmazione generativa permettendo a cose come il codice dettagliato e ripetitivo ma veloce di essere scritto in forme più brevi.


3

Sembra una questione di qualità dell'implementazione. Ad esempio, ecco cosa ho testato con:

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);
}

Con ManualInlinedefinito ottengo 28 secondi, ma senza ottengo 32. Quindi il compilatore non sta nemmeno incorporando questa semplice funzione, che penso sia chiaro che dovrebbe essere.

(La mia riga di comando è dmd -O -noboundscheck -inline -release ....)


1
I tuoi tempi sono insignificanti a meno che tu non dia anche il confronto ai tuoi tempi C ++.
deceleratedcaviar

3
@Daniel: ti manca il punto. Era per dimostrare le ottimizzazioni D in isolamento, in particolare per la conclusione che ho dichiarato: "Quindi il compilatore non sta nemmeno incorporando questa semplice funzione, che penso sia chiaro che dovrebbe essere". Sto anche tentando di confrontarlo con il C ++, come ho chiaramente affermato nella prima frase: "Sembra un problema di qualità dell'implementazione".
GManNickG,

Ah vero, scusa :). Scoprirai anche che il compilatore DMD non vettorializza affatto i loop.
deceleratedcaviar
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.