Еще одно сравнение производительности С++ и C#

Навеяно вот этой статьей.

Существует три мнения относительно производительности C++ и C#.

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

Люди из первой категории все время пытаются доказать свою правоту. При этом приводят примеры оптимизированного кода на C++ и самого пессимизированного кода на C#.

Пример «типичного» сравнения

image
Любой программист, который знает C# сразу увидит две ошибки:

  1. Вызов GC.Collect, который сводит на нет любые оптимизации сделанные в рантайме для сборки мусора.
  2. Использование for цикла, который гарантированно не устраняет проверки границ на каждое обращение к массиву.

При этом в реальности ни один C#-программист не напишет код с GC.Collect и очень малая часть программистов допустит ошибку в цикле for.
Какой смысл сравнивать гарантированного неэффективный код на C# даже с обычным кодом на C++? Разве что доказать свою точку зрения.

Честное сравнение

Чтобы сравнивать языки честно — надо сравнивать типовой код на обоих языках. То есть такой код, который можно встретить в программах с вероятностью больше статистической погрешности.

Для примера буду использовать ту же саму пузырьковую сортировку массива.

Тесты для C++

Для случая C++ протестирую три варианта:

  1. С-style массив (указатель)
  2. std::array
  3. 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#

Также сделаю три теста:

  1. Обычный массив
  2. Обычный массив с использованием unsafe (указателей)
  3. 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++ и использовании голых указателей. Но последнее чревато кучей ошибок.

ссылка на оригинал статьи http://habrahabr.ru/post/266373/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *