Насколько быстро 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 ...
.)