Насколько быстрый D по сравнению с C ++? - PullRequest
127 голосов
/ 28 февраля 2011

Мне нравятся некоторые функции D, но было бы интересно, если бы они пришли с штраф за время выполнения?

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

  • D: 18,9 с [см. Окончательное время выполнения ниже]
  • C ++: 3,8 с

Действительно ли C ++ почти в пять раз быстрее или я ошибся в D Программа

Я скомпилировал C ++ с g ++ -O3 (gcc-snapshot 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;
}

Ответы [ 8 ]

62 голосов
/ 28 февраля 2011

Чтобы включить все оптимизации и отключить все проверки безопасности, скомпилируйте вашу программу 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, чтобы они могли автоматически векторизовать цикл.

31 голосов
/ 01 марта 2011

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

У меня В последнее время приложили некоторые усилия для улучшения производительности GC , и результаты были довольно впечатляющими, по крайней мере, в синтетических тестах.Надеемся, что эти изменения будут включены в один из следующих нескольких выпусков и уменьшат проблему.

28 голосов
/ 02 марта 2011

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

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

13 голосов
/ 31 марта 2015

Определенно кажется проблемой качества реализации.

Я провел несколько тестов с кодом OP и внес некоторые изменения. Я на самом деле заставил D работать быстрее для LDC / clang ++, работая в предположении, что массивы должны выделяться динамически (xs и связанные скаляры). Ниже приведены некоторые цифры.

Вопросы для ОП

Намеренно ли использовать одно и то же начальное число для каждой итерации C ++, но не для D?

Настройка

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

После этого я внес следующие изменения:

  • Используется uninitializedArray, чтобы избежать начальных значений для скаляров в xs (возможно, это самое большое различие). Это важно, потому что D обычно по умолчанию вставляет все без вывода сообщений, чего нет в C ++.

  • Вычеркнут код печати и заменен writefln на writeln

  • Изменен импорт на выборочный
  • Использованный оператор pow (^^) вместо умножения вручную для последнего шага вычисления среднего
  • Удален size_type и заменен соответственно новым index_type псевдоним

... в результате получается 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;
    }

После тестирования 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 (модифицированный источник OP):

    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 (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 мс медленнее, чем scalar2.d, но быстрее, чем версия C ++.

Заключение

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

8 голосов
/ 03 марта 2011

dmd является эталонной реализацией языка, и поэтому большая часть работы отводится внешнему интерфейсу для исправления ошибок, а не оптимизации внутреннего интерфейса.

"in" быстрее в вашем случае, потому что вы используете динамические массивыявляются ссылочными типами.С ref вы вводите другой уровень косвенности (который обычно используется для изменения самого массива, а не только его содержимого).

Векторы обычно реализуются со структурами, в которых const ref имеет смысл.См. smallptD против smallpt для реального примера, показывающего множество векторных операций и случайностей.

Обратите внимание, что 64-разрядная версия также может иметь значение.Однажды я пропустил, что на x64 gcc компилирует 64-битный код, в то время как dmd по умолчанию все равно 32 (изменится, когда созреет 64-битный кодоген).Произошло замечательное ускорение с "dmd -m64 ...".

7 голосов
/ 28 февраля 2011

Скорее всего, 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 голосов
/ 01 марта 2011

Вы можете написать C-код D так, насколько он быстрее, это будет зависеть от многих вещей:

  • Какой компилятор вы используете
  • Какую функцию вы используете
  • насколько агрессивно вы оптимизируете

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

3 голосов
/ 01 марта 2011

Похоже на качество реализации вопроса.Например, вот что я тестировал:

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

...