Навеяно вот этой статьей.
Существует три мнения относительно производительности C++ и C#.
Те кто знают (или думают что знают) C++ считают, что C++ в разы или даже на порядки быстрее.
Те кто знают C++ и C# знают, что для обычных задач быстродействие C++ не нужно, а там где нужно — можно и C#-код заоптимизировать до невозможности. Верхний предел оптимизации у C++ выше, чем у C#, но такие рекорды никому не нужны.
Те кто знают только C# — никогда не испытывали проблем с его быстродействием.
Люди из первой категории все время пытаются доказать свою правоту. При этом приводят примеры оптимизированного кода на C++ и самого пессимизированного кода на C#.

Любой программист, который знает C# сразу увидит две ошибки:
При этом в реальности ни один C#-программист не напишет код с GC.Collect и очень малая часть программистов допустит ошибку в цикле for.
Какой смысл сравнивать гарантированного неэффективный код на C# даже с обычным кодом на C++? Разве что доказать свою точку зрения.
Чтобы сравнивать языки честно — надо сравнивать типовой код на обоих языках. То есть такой код, который можно встретить в программах с вероятностью больше статистической погрешности.
Для примера буду использовать ту же саму пузырьковую сортировку массива.
Для случая C++ протестирую три варианта:
Каждый тест будет запускаться по 100 раз и результат будет усредняться.
В тесте честно создается, заполняется, уничтожается массив и вызывается функция для его сортировки.
Код почти такой же как в первом случае, но теперь явно не освобождаем память, а отдаем все на откуп автоматической деструкции.
Кто знает C++ уже поняли, что std::array не вызывает аллокаций, а сам массив хранится в теле класса, то есть в стеке в этом примере. std::array должен стать однозначным лидером в этом забеге.
Код полностью аналогичен варианту с std::array. Но std::vector — изменяемый массив, в отличие от std::array. Поэтому vector использует динамическую память для хранения массива и должен честно проверять выход за границы.
Также сделаю три теста:
В отличие от «типичного» подхода сравнения производительности, я не буду вызывать GC.Collect, а положусь на рантайм.
Тесты будут прогоняться многократно, поэтому уборки мусора будут случаться и учитываться в замерах.
Очень важный момент — в ограничении цикла for стоит m.Length (минус константа). Такой паттерн определяется JIT и устраняет проверки на границу массива.
Сортировка выглядит так же, только используются указатели, и гарантированно никаких проверок (и никаких оптимизаций). Я не стал делать тест с fixed array, потому что в реальности его не встретишь.
В отличие от обычного массива, для List нет оптимизаций в JIT, поэтому значение длины списка храним в переменной.
Я компилировал код в Visual Studio 2015, запускал под .NET Framework 4.6. Везде настройки Release, по умолчанию.
Получил вот такие результат:
В режиме x86 оптимизатор полностью выкинул сортировку для std::array, поэтому получилось 0. В реальности работает чуть быстрее, чем C-style массив за счет отсутствия аллокаций.
Код можно найти тут — github.com/gandjustas/PerfTestCSharpVsCPP
В C# массив — ссылочный тип. Поэтому без проблем можно передавать в любую функцию. В C++ все контейнеры ведут себя как «размерные» типы и копируют все содержимое при передаче в качестве параметра. Нужно очень внимательно писать код для C++, чтобы не копировать лишний раз массивы. Зачастую придется прибегать к умным указателям, которые несут дополнительный оверхед.
В большом приложении такой оверхед сожрет весь выигрыш от использования C++. Заметно выиграть можно только на задачах с кучей математики, в которой компилятор C++ традиционно силен, или при отказе от идиом C++ и использовании голых указателей. Но последнее чревато кучей ошибок.
По просьбам читателей изменил и дополнил тесты:
Результаты получились такие:
Получается в .NET Native быстродействие такое же, как у C++.
Из-за тормозов в дебаге для std::swap отлаживать стало невозможно.
Исходники там же: github.com/gandjustas/PerfTestCSharpVsCPP
Существует три мнения относительно производительности C++ и C#.
Те кто знают (или думают что знают) C++ считают, что C++ в разы или даже на порядки быстрее.
Те кто знают C++ и C# знают, что для обычных задач быстродействие C++ не нужно, а там где нужно — можно и C#-код заоптимизировать до невозможности. Верхний предел оптимизации у C++ выше, чем у C#, но такие рекорды никому не нужны.
Те кто знают только C# — никогда не испытывали проблем с его быстродействием.
Люди из первой категории все время пытаются доказать свою правоту. При этом приводят примеры оптимизированного кода на C++ и самого пессимизированного кода на C#.
Пример «типичного» сравнения

Любой программист, который знает C# сразу увидит две ошибки:
- Вызов GC.Collect, который сводит на нет любые оптимизации сделанные в рантайме для сборки мусора.
- Использование for цикла, который гарантированно не устраняет проверки границ на каждое обращение к массиву.
При этом в реальности ни один C#-программист не напишет код с GC.Collect и очень малая часть программистов допустит ошибку в цикле for.
Какой смысл сравнивать гарантированного неэффективный код на C# даже с обычным кодом на C++? Разве что доказать свою точку зрения.
Честное сравнение
Чтобы сравнивать языки честно — надо сравнивать типовой код на обоих языках. То есть такой код, который можно встретить в программах с вероятностью больше статистической погрешности.
Для примера буду использовать ту же саму пузырьковую сортировку массива.
Тесты для C++
Для случая C++ протестирую три варианта:
- С-style массив (указатель)
- std::array
- std::vector
Каждый тест будет запускаться по 100 раз и результат будет усредняться.
Код измерения
std::chrono::high_resolution_clock::duration measure(std::function<void()> f, int n = 100)
{
auto begin = std::chrono::high_resolution_clock::now();
for (int i = 0; i < n; i++)
{
f();
}
auto end = std::chrono::high_resolution_clock::now();
return (end - begin) / n;
}
Код теста для C-style
Код
void c_style_sort(int *m, int n)
{
for (int i = 0; i < N - 1; i++)
for (int j = i + 1; j < N; j++) {
if (m[i] < m[j])
{
int tmp = m[i];
m[i] = m[j];
m[j] = tmp;
}
}
}
void c_style_test()
{
int* m = new int[N];
for (int i = 0; i < N; i++)
{
m[i] = i;
}
c_style_sort(m, N);
delete[] m;
}
В тесте честно создается, заполняется, уничтожается массив и вызывается функция для его сортировки.
Код теста для std::array
Код
void cpp_style_sort(std::array<int, N> &m)
{
auto n = m.size();
for (int i = 0; i < n-1; i++)
for (int j = i + 1; j < n; j++) {
if (m[i] < m[j])
{
int tmp = m[i];
m[i] = m[j];
m[j] = tmp;
}
}
}
void cpp_style_test()
{
std::array<int, N> m;
for (int i = 0; i < N; i++)
{
m[i] = i;
}
cpp_style_sort(m);
}
Код почти такой же как в первом случае, но теперь явно не освобождаем память, а отдаем все на откуп автоматической деструкции.
Кто знает C++ уже поняли, что std::array не вызывает аллокаций, а сам массив хранится в теле класса, то есть в стеке в этом примере. std::array должен стать однозначным лидером в этом забеге.
Код теста для std::vector
Код
void vector_sort(std::vector<int> &m)
{
auto n = m.size();
for (int i = 0; i < n - 1; i++)
for (int j = i + 1; j < n; j++) {
if (m[i] < m[j])
{
int tmp = m[i];
m[i] = m[j];
m[j] = tmp;
}
}
}
void vector_test()
{
std::vector<int> m;
m.reserve(N);
for (int i = 0; i < N; i++)
{
m.push_back(i);
}
vector_sort(m);
}
Код полностью аналогичен варианту с std::array. Но std::vector — изменяемый массив, в отличие от std::array. Поэтому vector использует динамическую память для хранения массива и должен честно проверять выход за границы.
Тесты для C#
Также сделаю три теста:
- Обычный массив
- Обычный массив с использованием unsafe (указателей)
- System.Collections.Generic.List
В отличие от «типичного» подхода сравнения производительности, я не буду вызывать GC.Collect, а положусь на рантайм.
Тесты будут прогоняться многократно, поэтому уборки мусора будут случаться и учитываться в замерах.
Код измерения
static long Measure(Action f, int n = 100)
{
var sw = System.Diagnostics.Stopwatch.StartNew();
for (int i = 0; i < n; i++)
{
f();
}
return sw.ElapsedMilliseconds / n;
}
Код теста для обычного массива
Код
static void ArrayTest()
{
var m = new int[N];
for (int i = 0; i < m.Length; i++)
{
m[i] = i;
}
ArraySort(m);
}
static void ArraySort(int[] m)
{
for (int i = 0; i < m.Length - 1; i++)
for (int j = i + 1; j < m.Length; j++)
{
if (m[i] < m[j])
{
int tmp = m[i];
m[i] = m[j];
m[j] = tmp;
}
}
}
Очень важный момент — в ограничении цикла for стоит m.Length (минус константа). Такой паттерн определяется JIT и устраняет проверки на границу массива.
Код теста для unsafe
Код
static unsafe void UnsafeTest()
{
var m = new int[N];
fixed(int* ptr = &m[0])
{
for (int i = 0; i < N; i++)
{
ptr[i] = i;
}
UnsafeSort(ptr, N);
}
}
static unsafe void UnsafeSort(int* m, int n)
{
for (int i = 0; i < n - 1; i++)
for (int j = i + 1; j < n; j++)
{
if (m[i] < m[j])
{
int tmp = m[i];
m[i] = m[j];
m[j] = tmp;
}
}
}
Сортировка выглядит так же, только используются указатели, и гарантированно никаких проверок (и никаких оптимизаций). Я не стал делать тест с fixed array, потому что в реальности его не встретишь.
Код теста для List
Код
static void ListTest()
{
var m = new List<int>(N);
for (int i = 0; i < N; i++)
{
m.Add(i);
}
ListSort(m);
}
static void ListSort(List<int> m)
{
var n = m.Count;
for (int i = 0; i < n - 1; i++)
for (int j = i + 1; j < n; j++)
{
if (m[i] < m[j])
{
int tmp = m[i];
m[i] = m[j];
m[j] = tmp;
}
}
}
В отличие от обычного массива, для List нет оптимизаций в JIT, поэтому значение длины списка храним в переменной.
Результаты
Я компилировал код в Visual Studio 2015, запускал под .NET Framework 4.6. Везде настройки Release, по умолчанию.
Получил вот такие результат:
Тест | x86 | x64 |
---|---|---|
(С++) С-style | 55ms | 55ms |
(С++) std::array | 0ms (52ms) | 65ms |
(С++) std::vector | 100ms | 65ms |
(C#) массив | 67ms | 90ms |
(C#) unsafe массив | 63ms | 105ms |
(C#) List | 395ms | 390ms |
В режиме x86 оптимизатор полностью выкинул сортировку для std::array, поэтому получилось 0. В реальности работает чуть быстрее, чем C-style массив за счет отсутствия аллокаций.
Выводы
- Для обоих языков идиоматичный код — самый эффективный (вообще странно если бы это было не так)
- C# медленнее C++ в таких задачах на 20%-50% (это верхняя граница)
- Для x64 оптимизировать надо отдельно (очевидно, но все же)
Код можно найти тут — github.com/gandjustas/PerfTestCSharpVsCPP
Что осталось «за кадром»
В C# массив — ссылочный тип. Поэтому без проблем можно передавать в любую функцию. В C++ все контейнеры ведут себя как «размерные» типы и копируют все содержимое при передаче в качестве параметра. Нужно очень внимательно писать код для C++, чтобы не копировать лишний раз массивы. Зачастую придется прибегать к умным указателям, которые несут дополнительный оверхед.
В большом приложении такой оверхед сожрет весь выигрыш от использования C++. Заметно выиграть можно только на задачах с кучей математики, в которой компилятор C++ традиционно силен, или при отказе от идиом C++ и использовании голых указателей. Но последнее чревато кучей ошибок.
Update
По просьбам читателей изменил и дополнил тесты:
- Использовал std::swap вместо ручного обмена в C++ коде
- Сделал вызов .at для вектора вместо [], чтобы он проверял границы в релизной сборке
- Добавил проект с .NET Native
Результаты получились такие:
Тест | x86 | x64 |
---|---|---|
(С++) С-style | 60ms | 52ms |
(С++) std::array | 51ms | 60ms |
(С++) std::vector | 147ms | 81ms |
(C#) массив | 67ms | 90ms |
(C#) unsafe массив | 63ms | 105ms |
(C#) List | 395ms | 390ms |
(C# + .NET Native) массив | 62ms | 59ms |
(C# + .NET Native) unsafe массив | 63ms | 52ms |
(C# + .NET Native) List | 274ms | 282ms |
Получается в .NET Native быстродействие такое же, как у C++.
Из-за тормозов в дебаге для std::swap отлаживать стало невозможно.
Исходники там же: github.com/gandjustas/PerfTestCSharpVsCPP