Наскільки швидко D у порівнянні з C ++?


133

Мені подобаються деякі функції D, але мені було б цікаво, якщо вони прийдуть зі штрафним виконанням?

Для порівняння я реалізував просту програму, яка обчислює скалярні продукти багатьох коротких векторів як у С ++, так і в D. Результат дивовижний:

  • D: 18,9 с [див. Нижче для остаточного виконання]
  • C ++: 3,8 с

Чи справді C ++ майже майже в п'ять разів швидше чи я помилився в програмі D?

Я компілював C ++ з g ++ -O3 (gcc-знімок 2011-02-19) і D з dmd -O (dmd 2.052) на помірному недавньому робочому столі Linux. Результати можна відтворити протягом декількох пробіжок, а стандартні відхилення незначні.

Ось програма 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;
}

І ось версія 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
До речі, у вашій програмі є помилка в цьому рядку: avg = avg / N*N(порядок операцій).
Володимир Пантелеєв

4
Можна спробувати переписати код, використовуючи масив / векторні операції digitalmars.com/d/2.0/arrays.html
Michal

10
Для кращого порівняння ви повинні використовувати той самий компілятор бек-енд. Або DMD і DMC ++, або GDC і G ++
he_the_great

1
@Sion Sheevok На жаль, видається, що dmd-профілірування не доступне для Linux? (Будь ласка, виправте мене, якщо я помиляюся, але якщо я скажу, dmd ... trace.defщо отримую error: unrecognized file extension def. І в документах dmd для optlink згадується тільки Windows.
Ларс,

1
Ах, ніколи про це не піклувався .def файл, який він випльовує. Часи синхронізації знаходяться у файлі .log. "Він містить перелік функцій у тому порядку, коли лінкер повинен їх зв'язати" - можливо, це допомагає optlink для оптимізації чогось? Також зауважте, що "Крім того, ld повністю підтримує стандартні файли" * .def ", які можуть бути вказані в командному рядку лінкера, як файл об'єкта" - так що ви можете спробувати передати trace.def через -L, якщо ви дорого хочете до.
Trass3r

Відповіді:


64

Щоб увімкнути всі оптимізації та відключити всі перевірки безпеки, скомпілюйте програму D із такими прапорами DMD:

-O -inline -release -noboundscheck

EDIT : Я спробував ваші програми з g ++, dmd та gdc. dmd відстає, але gdc досягає продуктивності дуже близької до g ++. Я використовував командний рядок gdmd -O -release -inline(gdmd - обгортка навколо gdc, яка приймає параметри dmd).

Дивлячись на лістинг асемблера, схоже, що ні dmd, ні gdc вбудовані scalar_product, але g ++ / gdc не випускає інструкцій MMX, тому вони можуть бути автоматично векторизованими циклом.


3
@CyberShadow: але якщо ви видалите перевірку безпеки ... чи не втрачаєте ви важливих особливостей D?
Матьє М.

33
Ви втрачаєте функції, яких у C ++ ніколи не було. Більшість мов не дають вам вибору.
Володимир Пантелеев,

6
@CyberShadow: чи можна вважати це як своєрідну збірку налагодження проти випуску?
Франческо

7
@Bernard: у звільненні, перевірка меж вимкнена для всіх кодів, крім безпечних функцій. щоб дійсно вимкнути межі перевірки, використовуйте як -випустіть, і -необменовано.
Міхал Мініч

5
@CyberShadow Дякую! З цими прапорами час роботи значно покращується. Зараз D знаходиться в 12,9 с. Але все одно працює більше ніж у 3 рази. @Matthieu M. Я не був би проти тестувати програму з перевіркою частоти в повільному темпі руху, і як тільки вона буде налагоджена, нехай вона виконує свої обчислення, не перевіряючи сумнівів. (Я роблю те саме з C ++ зараз.)
Ларс

32

Одна велика річ, яка сповільнює D - це реалізація сміття з підрозділу. Орієнтовні показники, які не сильно підкреслюють GC, демонструватимуть дуже схожу ефективність з кодом C та C ++, зібраним з тим самим доповненням компілятора. Орієнтовні показники, які сильно напружують GC, покажуть, що D виконує безвідмовно. Будьте впевнені, однак, це єдине (хоч і серйозне) питання якості впровадження, а не гарантія повільності. Крім того, D надає можливість відмовитися від GC та налаштувати управління пам’яттю на критичні для продуктивності біти, при цьому все ще використовуючи його в менш критичних для продуктивності 95% коду.

Останнім часом я доклав певних зусиль для покращення ефективності роботи GC, і результати були досить драматичними, принаймні, щодо синтетичних орієнтирів. Сподіваємось, ці зміни будуть інтегровані в один з наступних кількох випусків і пом’якшать проблему.


1
Я помітив, що однією з ваших змін була перехід від поділу до бітового зсуву. Чи не повинен це робити щось, що робить компілятор?
GManNickG

3
@GMan: Так, якщо значення, на яке ви ділите, відоме під час компіляції. Ні, якщо значення відомо лише під час виконання, саме тоді я здійснив оптимізацію.
dimimcha

@dsimcha: Гм. Я думаю, якщо ви знаєте, як це зробити, компілятор також може. Якість питання щодо впровадження чи я пропускаю, що потрібно виконати певну умову, що компілятор не може довести, але ви знаєте? (Я зараз вчу D, тож ці дрібниці щодо компілятора раптом мені цікаві.))
GManNickG

13
@GMan: Перемикання бітів працює лише в тому випадку, якщо число, на яке ви ділите, - це два. Компілятор не може довести це, якщо число відоме лише під час виконання, а тестування та розгалуження проходитиме повільніше, ніж просто використання команди div. Мій випадок незвичний, тому що значення відомо лише під час виконання, але я знаю, що під час компіляції це буде потужність у два.
dimimcha

7
Зауважте, що програма, розміщена в цьому прикладі, не здійснює розподіл у трудомісткій частині.
Володимир Пантелеєв

27

Це дуже повчальна нитка, дякую за всю роботу ОП та помічникам.

Одне зауваження - цей тест не оцінює загальне питання абстракції / покарання функції або навіть питання про якість резервного копіювання. Він зосереджений на практично одній оптимізації (циклічна оптимізація). Я вважаю, що справедливо сказати, що бекенд Gcc дещо вишуканіший, ніж dmd, але було б помилкою вважати, що розрив між ними такий же великий для всіх завдань.


4
Я повністю згоден. Як додано пізніше, мене в основному цікавить ефективність для чисельних обчислень, де оптимізація циклу, ймовірно, є найбільш важливою. Які інші оптимізації, на вашу думку, були б важливими для чисельних обчислень? І які обчислення випробували б їх? Мені буде цікаво доповнити свій тест і виконати ще кілька тестів (якщо вони будуть приблизно такими ж простими). Але evtl. це вже власне питання?
Ларс

11
Як інженер, який різав зуби на C ++, ви мій герой. Однак з повагою це повинен бути коментар, а не відповідь.
Алан

14

Однозначно здається, що питання якості реалізації.

Я провів кілька тестів з кодом ОП і вніс деякі зміни. Насправді я D швидше йшов для LDC / clang ++, працюючи на припущенні, що масиви повинні бути розподілені динамічно ( xsта пов'язані з ними скаляри). Дивіться нижче деякі номери.

Питання до ОП

Чи є навмисним використання одного і того ж насіння для кожної ітерації C ++, тоді як для D не так?

Налаштування

Я переробив оригінальне джерело D (охрещене scalar.d), щоб зробити його портативним між платформами. Це стосувалося лише зміни типу чисел, які використовуються для доступу та зміни розміру масивів.

Після цього я вніс такі зміни:

  • Використовується uninitializedArrayдля уникнення inits за замовчуванням для скалярів у xs (мабуть, це зробило найбільше значення) Це важливо, оскільки D зазвичай за замовчуванням вводить усе безшумно, чого C ++ не робить.

  • Факторний код друку і замінений writeflnнаwriteln

  • Змінив імпорт на вибірковий
  • Використовується оператор pow ( ^^) замість ручного множення для останнього кроку обчислення середнього
  • Вилучено size_typeта замінено належним чином новим index_typeпсевдонімом

... таким чином отримуємоscalar2.cpp ( пастбін ):

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

Після тестування scalar2.d(який по пріоритетам оптимізації швидкості), з цікавості я замінив петлю в mainс foreachеквівалентами, і назвав його 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;
    }

Я склав кожен з цих тестів, використовуючи компілятор на основі LLVM, оскільки LDC є найкращим варіантом для компіляції D з точки зору продуктивності. У моїй установці x86_64 Arch Linux я використав такі пакети:

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

Я використовував наступні команди для складання кожної:

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

Результати

Результати ( скріншот вихідного консольного виводу ) кожної версії джерела такі:

  1. scalar.cpp (оригінальний C ++):

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

    C ++ встановлює стандарт на рівні 2582 мс .

  2. scalar.d (змінене джерело ОП):

    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 

    Це тривало ~ 2957 мс . Повільніше, ніж реалізація C ++, але не дуже.

  3. scalar2.d (зміна типу індексу / довжини та неініціалізована оптимізація масиву):

    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

    Іншими словами, ~ 1860 мс . Поки що це лідирує.

  4. scalar3.d (передвіщає):

    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 мс повільніше scalar2.d, але швидше, ніж версія C ++.

Висновок

При правильних оптимізаціях реалізація D фактично пішла швидше, ніж її еквівалентна C ++ реалізація, використовуючи доступні компілятори на основі LLVM. Поточний розрив між D та C ++ для більшості програм видається лише заснованим на обмеженнях поточних реалізацій.


8

dmd є базовою реалізацією мови, і тому більшість робіт вкладається у фронтенд, щоб виправити помилки, а не оптимізувати бекенд.

"in" у вашому випадку швидше, тому що ви використовуєте динамічні масиви, які є еталонними типами. З посиланням ви вводите інший рівень непрямості (який зазвичай використовується для зміни самого масиву, а не лише вмісту).

Зазвичай вектори реалізуються з конструкціями, де const ref має ідеальний сенс. Див. SmallptD vs. smallpt на прикладі реального світу, що містить навантаження векторних операцій та випадковості.

Зауважте, що 64-розрядні можуть також змінити значення. Я колись пропустив, що на x64 gcc компілює 64-розрядний код, тоді як dmd все ще за замовчуванням до 32 (зміниться, коли 64-розрядний кодеген дозріває). Відбулося надзвичайне прискорення з "dmd -m64 ...".


7

Чи швидше C ++ чи D, швидше за все, буде сильно залежати від того, що ви робите. Я б подумав, що, порівнюючи добре написаний C ++ з добре написаним кодом D, вони, як правило, або мають однакову швидкість, або C ++ будуть швидшими, але те, що конкретний компілятор вдається оптимізувати, може мати великий ефект цілком осторонь мови себе.

Однак є кілька випадків, коли D має великі шанси перемогти С ++ за швидкість. Основним, що спадає на думку, буде обробка рядків. Завдяки можливостям нарізки масиву D рядки (і масиви взагалі) можуть бути оброблені набагато швидше, ніж ви легко можете зробити в C ++. Що стосується D1, XML-процесор Tango надзвичайно швидкий , насамперед завдяки можливостям нарізки масиву D (і, сподіваємось, D2 матиме аналогічно швидкий аналізатор XML після того, як буде завершено те, що зараз працює над Phobos). Отже, в кінцевому підсумку, чи буде D або C ++ швидше, буде дуже залежно від того, що ви робите.

Тепер я буду здивований , що ви бачите таку різницю в швидкості в даному конкретному випадку, але це така справа , що я б очікувати поліпшення , як DMD поліпшується. Використання gdc може принести кращі результати і, швидше за все, буде більш детальним порівнянням мови (а не бекенда), враховуючи, що вона заснована на gcc. Але мене це зовсім не здивує, якщо є ряд речей, які можна зробити для прискорення коду, який генерує dmd. Я не думаю, що в цьому моменті є багато питань, що gcc зріліший ніж dmd. А оптимізація коду є одним із головних плодів зрілості коду.

Зрештою, важливим є те, наскільки добре працює dmd для вашої конкретної програми, але я погоджуюся, що було б непогано знати, наскільки добре C ++ і D порівнюються загалом. Теоретично вони повинні бути майже однаковими, але це реально залежить від реалізації. Я думаю, що для того, щоб перевірити, наскільки добре вони в даний час, вони потрібні для вичерпного набору орієнтирів.


4
Так, я був би здивований, якби введення / виведення було значно швидшим на будь-якій мові, або якби чиста математика була значно швидшою на будь-якій мові, але струнні операції, управління пам'яттю та кілька інших речей могли легко дати одній мові світити.
Макс Лібберт

1
Це зробити краще (швидше), ніж іострими C ++. Але це насамперед питання впровадження бібліотеки (для всіх відомих версій від найпопулярніших постачальників).
Ben Voigt

4

Ви можете написати C-код D, наскільки це швидше, це залежатиме від багатьох речей:

  • Який компілятор ви використовуєте
  • Яку функцію ви використовуєте
  • як агресивно ви оптимізуєте

Відмінності у першому не є справедливим для перетягування. Другий може дати перевагу C ++, оскільки він, якщо що, має менше важких особливостей. Третє - найцікавіше: D-код в чомусь легше оптимізувати, оскільки загалом його легше зрозуміти. Крім того, він має можливість робити велику ступінь генеративного програмування, що дозволяє такі речі, як багатослівний та повторюваний, але швидкий код, записуватись у коротших формах.


3

Схоже, питання якості впровадження. Наприклад, ось що я тестував:

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

З ManualInlineпевним значенням я отримую 28 секунд, але без мене стає 32. Тож компілятор навіть не вбудовує цю просту функцію, що, я думаю, зрозуміло, що має бути.

(Мій командний рядок є dmd -O -noboundscheck -inline -release ....)


1
Ваші таймінги є безглуздими, якщо ви також не порівняєте свої C ++.
deceleratedcaviar

3
@Daniel: Ви пропускаєте суть. Потрібно було продемонструвати оптимізацію D окремо, а саме для висновку я зазначив: "Отже, компілятор навіть не включає цю просту функцію. Я думаю, це зрозуміло, що вона повинна бути". Я навіть намагаюся порівняти це з C ++, як я чітко зазначив у першому реченні: "Схоже, питання якості реалізації".
GManNickG

Ах правда, вибачте :). Ви також знайдете, що компілятор DMD взагалі не векторизує цикли.
deceleratedcaviar
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.