Насколько быстро D сравнивается с С++?

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

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

  • D: 18,9 с [см. ниже для окончательной версии]
  • С++: 3.8 s

Является ли С++ действительно почти в пять раз быстрее, или я ошибаюсь в D программа?

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

Здесь программа С++:

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

Ответ 1

Чтобы включить все оптимизации и отключить все проверки безопасности, скомпилируйте свою программу D со следующими флагами DMD:

-O -inline -release -noboundscheck

EDIT. Я пробовал ваши программы с помощью g++, dmd и gdc. dmd отстает, но gdc достигает производительности очень близко к g++. Командная строка, которую я использовал, была gdmd -O -release -inline (gdmd - обертка вокруг gdc, которая принимает параметры dmd).

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

Ответ 2

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

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

Ответ 3

Это очень поучительный поток, благодаря всей работе с OP и помощниками.

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

Ответ 4

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

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

Вопросы для OP

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

Настройка

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

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

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

  • Замените код печати и замените 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

Я использовал следующие команды для компиляции:

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

Результаты

Результаты (снимок экрана вывода исходной консоли) каждой версии источника следующим образом:

  • scalar.cpp (исходный С++):

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

    С++ устанавливает стандарт в 2582 мс.

  • 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 мс. Медленно, чем реализация на С++, но не слишком много.

  • 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 мс. Пока это лидирует.

  • 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, но быстрее, чем версия С++.

Заключение

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

Ответ 5

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

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

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

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

Ответ 6

Быстрее ли С++ или D, скорее всего, будет сильно зависеть от того, что вы делаете. Я бы подумал, что при сравнении хорошо написанного С++ с хорошо написанным кодом D они обычно либо будут иметь одинаковую скорость, либо С++ будет быстрее, но то, что конкретный компилятор может оптимизировать, может иметь большой эффект полностью, кроме языка сам по себе.

Однако есть несколько случаев, когда D имеет хорошие шансы на избиение С++ для скорости. Главное, что приходит в голову, - это обработка строк. Благодаря возможности разбиения массива D, строки (и массивы в целом) могут обрабатываться намного быстрее, чем вы можете легко сделать на С++. Для D1 Процессор Tango XML чрезвычайно быстр, в первую очередь благодаря возможности массива D массивов (и, надеюсь, у D2 будет такой же быстрый XML-анализатор, как только тот, который в настоящее время разрабатывается для Phobos, завершен). Таким образом, в конечном счете, будет ли D или С++ быстрее, будет очень зависеть от того, что вы делаете.

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

В конечном счете, важно то, насколько хорошо dmd выполняет для вашего конкретного приложения, но я согласен с тем, что было бы неплохо узнать, насколько хорошо С++ и D сравниваются в целом. Теоретически, они должны быть почти одинаковыми, но это действительно зависит от реализации. Я думаю, что для того, чтобы действительно проверить, насколько хорошо эти два в настоящее время сравниваются, потребуется полный набор критериев.

Ответ 7

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

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

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

Ответ 8

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

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