Например, сложно поспорить с тем, что код C# может работать быстрее за счет оптимизации под платформу во время JIT компиляции. Или например с тем, что ядро .Net Framework само по себе очень хорошо оптимизировано.
С другой стороны, весомым аргументом является то, что С++ компилируется непосредственно в машинный код и работает с минимально возможным количеством хелперов и прослоек.
Встречаются и мнения о том, что производительность кода измерять не правильно, ибо микро уровень не характеризует производительность на макро уровне. (я конечно соглашусь с тем что на макроуровне можно испортить производительность, но вряд ли соглашусь с тем что производительность на макро-уровне не складывается из производительности на микро-уровне)
Попадались и утверждения о том, что код на С++ примерно в десять раз быстрее кода на С#.
Все это многообразие противоречивых мнений приводит к мысли о том, что нужно самому попробовать написать максимально идентичный и простой код на одном и другом языке, и сравнить время его выполнения. Что и было мною сделано.
Тест, который выполнен в этой статье
Мне хотелось выполнить самый примитивный тест, который покажет разницу между языками на микро-уровне. В тесте пройдем полный цикл операций с данными, создание контейнера, заполнение, обработка и удаление, т.е. как обычно и бывает в приложениях.
Работать будем с данными типа int, дабы сделать их обработку максимально идентичной. Сравнивать будем только релизные билды дефолтной конфигурации используя Visual Studio 2010.
Код будет выполнять следующие действия:
1. Аллоцирование массива\контейнера
2. Заполнение массива\контейнера числами по возрастанию
3. Сортировка массива\контейнера методом пузырька по убыванию (метод выбран самый простой, по скольку мы не сравниваем методы сортировки, а средства реализации)
4. Удаление массива\контейнера
Код была написан несколькими альтернативными методами, отличающимися различными типами контейнеров и методами их аллокации. В самой статье приведу лишь примеры кода, которые как правило, работали максимально быстро для каждого из языков. Остальные же примеры, со вставками для подсчета скорости выполнения,- полностью можно увидеть тут.
Код теста
С++ HeapArray | С# HeapArray fixed tmp |
Как вы можете убедиться, код достаточно простой и почти идентичный. Поскольку в C# нельзя явно выполнить удаление, время выполнения которого мы хотим измерить, вместо удаления будем использовать items = null; GC.Collect(); при условии что ничего кроме контейнера мы (во всем нашем примере) не создавали, GC.Collect удалить должен бы тоже только контейнер, поэтому думаю это достаточно адекватная замена delete[] items.
Объявление int tmp; за циклом в случае C# экономит время, поэтому рассмотрена именно такая вариация теста для случая C#.
На разных машинах получались разные результаты данного теста (видимо в силу разницы архитектур), однако разницу в производительности кода результаты измерений позволяют оценить.
В измерениях, для подсчета времени выполнения кода, был использован QueryPerformanceCounter, измерялось «время» создания, заполнения, сортировки и удаления на тестовых платформах формах получились следующие результаты:
Из таблиц видно что:
1. Cамая быстрая С# реализация работает медленнее самой быстрой C++ реализации на 30-60% (в зависимости от платформы)
2. Разброс между самой быстрой и самой медленной С++ реализацией 1-65% (в зависимости от платформы)
3. Самая медленная(из рассмотренных конечно) реализация на С#, медленнее самой медленной С++ реализации примерно в 4 раза
4. Больше всего времени занимает этап сортировки (по сему, в дальнейшем рассмотрим его более детально)
Еще стоит обратить внимание на то, что std::vector является медленным контейнером на старых платформах, однако вполне быстрым на современных. А также на то, что время «удаления» в случае первого .Net теста несколько выше, видимо из-за того что кроме тестовых данных удаляются еще какие-то сущности.
Причина разницы производительности С++ и С# кода
Давайте посмотрим на код, который выполняется процессором в каждом случае. Для этого возьмем код сортировки из самых быстрых примеров и посмотрим во что он компилируется, смотреть будем используя отладчик Visual Studio 2010 и режим disassembly, в результате для сортировки увидим следующий код:
С++ | С# |
for(int i=0;i<10000;i++) 00F71051 xor ebx,ebx 00F71053 mov esi,edi for(int j=i;j<10000;j++) 00F71055 mov eax,ebx 00F71057 cmp ebx,2710h 00F7105D jge HeapArray+76h (0F71076h) 00F7105F nop { if(items[i] < items[j]) 00F71060 mov ecx,dword ptr [edi+eax*4] 00F71063 mov edx,dword ptr [esi] 00F71065 cmp edx,ecx 00F71067 jge HeapArray+6Eh (0F7106Eh) { int tmp = items[j]; items[j] = items[i]; 00F71069 mov dword ptr [edi+eax*4],edx items[i] = tmp; 00F7106C mov dword ptr [esi],ecx for(int j=i;j<10000;j++) 00F7106E inc eax 00F7106F cmp eax,2710h 00F71074 jl HeapArray+60h (0F71060h) for(int i=0;i<10000;i++) 00F71076 inc ebx 00F71077 add esi,4 00F7107A cmp ebx,2710h 00F71080 jl HeapArray+55h (0F71055h) } } |
int tmp; for (int i = 0; i < 10000; i++) 00000076 xor edx,edx 00000078 mov dword ptr [ebp-38h],edx for (int j = i; j < 10000; j++) 0000007b mov ebx,dword ptr [ebp-38h] 0000007e cmp ebx,2710h 00000084 jge 000000BB 00000086 mov esi,dword ptr [edi+4] { if (items[i] < items[j]) 00000089 mov eax,dword ptr [ebp-38h] 0000008c cmp eax,esi 0000008e jae 000001C2 00000094 mov edx,dword ptr [edi+eax*4+8] 00000098 cmp ebx,esi 0000009a jae 000001C2 000000a0 mov ecx,dword ptr [edi+ebx*4+8] 000000a4 cmp edx,ecx 000000a6 jge 000000B0 000000a8 mov dword ptr [edi+ebx*4+8],edx items[i] = tmp; 000000ac mov dword ptr [edi+eax*4+8],ecx for (int j = i; j < 10000; j++) 000000b0 add ebx,1 000000b3 cmp ebx,2710h 000000b9 jl 00000089 for (int i = 0; i < 10000; i++) 000000bb inc dword ptr [ebp-38h] 000000be cmp dword ptr [ebp-38h],2710h 000000c5 jl 0000007B } } |
Что мы тут можем увидеть?
19 инструкций C++ против 23 на С#, разница не большая, но в купе с прочей оптимизацией, думаю она может объяснить причину большего времени выполнения C# кода.
В C# реализации также некоторые вопросы вызывает
jae 000001C2, который выполняет переход
на 000001c2 call 731661B1
Который видимо также и влияет на разницу во времени выполнения, внося дополнительные задержки.
Другие сравнения производительности
Стоит отметить что есть и другие статьи, где измеряли производительность С++ и С#. Из тех, что попадались мне, самой содержательной показалась Head-to-head benchmark: C++ vs .NET
Автор этой статьи, в некоторых тестах «подыграл» C# запретив использовать SSE2 для С++, поэтому некоторые результаты С++ тестов с плавающей стали примерно в два раза медленнее чем были бы с включенным SSE2. В статье можно найти и другую критику методологии автора, среди которой очень субъективный выбор контейнера для теста в С++.
Однако не принимая в расчет тесты с плавающей точкой без SSE2, и делая поправку на ряд других особенностей методики тестирования, результаты, полученные в статье, стоит рассмотреть.
По результатам измерений можно сделать ряд интересных выводов:
1. Дебажный билд С++ заметно медленнее релизного, при том что разница дебажного и релизного билда С# менее существенна
2. Производительность C# под .Net Framework заметно(более 2х раз) выше чем производительность под Mono
3. Для С++ вполне можно найти контейнер который будет работать медленнее подобного контейнера для C#, и никакая оптимизация не поможет это побороть кроме как использование другого контейнера
4. Некоторые операции работы с файлом в С++ заметно медленнее аналогов в С#, однако их альтернативы столь же заметно быстрее аналогов С#.
Если подводить итоги и говорить о Windows, то статья приходит примерно к похожим результатам: код С# медленнее С++ кода, примерно на 10-80%
Много ли это -10..-80%?
Допустим при разработке на С# мы всегда будем использовать наиболее оптимальное решение, что потребует от нас очень неплохих навыков. И предположим мы будем укладываться в суммарные 10..80% потерь производительности производительности. Чем это нам грозит? Попробуем сравнить эти проценты с другими показателями характеризующими производительность.
Например, в 1990-2000 годах, одно-поточная производительность процессора росла за год примерно на 50%. А начиная с 2004 года темпы роста производительности процессоров упали, и составляли лишь 21% в год, по крайней мере до 2011 года.
A Look Back at Single-Threaded CPU Performance
Ожидаемые показатели роста производительности весьма туманны. Вряд ли в 2013 и 2014 годах был показан рост выше 21%, более того, вполне вероятно что в будущем рост ожидается еще ниже. По крайней мере, планы Intel по осваиванию новых технологий с каждым годом все скромнее…
Другое направление для оценки,- это энергоэффективность и дешевизна железа. Например тут можно увидеть, что говоря о топовом железе +50% одно-поточной производительности может в 2-3 раза удорожать стоимость процессора.
C точки же зрения энергоэфективности и шума — сейчас вполне реально собрать экономичный PC на пассивном охлаждении, однако придется пожертвовать производительностью, и эта жертва вполне может быть около 50% и более производительности относительно прожорливого и горячего, но производительного железа.
Как будет расти производительность процессоров точно не известно, однако по оценкам видно что в случае 21% роста производительности в год, приложение на С#, может отставать по производительности на 0.5-4 года относительно приложения на С++. В случае, например 10% роста, — отставание уже будет 1-8 лет. Однако, реальное приложение может отставать намного меньше, ниже рассмотрим почему.
Я пока не берусь оценивать рентабельность жертвы 10..80% производительности ради получения экономии на разработке. Очевидно, что эта рентабельность зависит от стоимости получения этих 10..80% другими способами (т.е. за счет железа). Однако наметившаяся тенденция показывает, что каждый следующий процент производительности железа будет дороже предыдущего, что, вполне вероятно, рано или поздно приведет к ситуации, когда дешевле будет получить дополнительную производительность оптимизируя код.
Какая же все-таки реальная оценка?
С одной стороны вы вряд-ли будете писать столь оптимальный чтобы всегда показывать максимальную производительность.
Но с другой стороны, что более важно: сколько runtime (времени выполнения) вашей программы будет занимать ваш код, а сколько код системы?
Например если код занимает 1% времени выполнения приложения или сервиса, то даже 10-ти кратное падение производительности этого кода, не очень сильно повлияло бы на скорость работы приложения, и удар по производительности был бы лишь около 10%.
Но совсем другое дело когда около 100% времени выполнения приложения занимает выполнение вашего кода, а не кода ОС. В этом случае вы легко можете получить и -80% и большие потери производительности.
Выводы
Конечно из всего выше написанного не следует что нужно срочно переходить с С# на С++. Во первых разработка на C# дешевле, а во вторых для ряда задач производительность современных процессоров избыточна, и даже оптимизация в рамках C# не является нужной. Но мне кажется важным обратить внимание на накладные расходы т.е плату за использование managedсреды, и оценку этих расходов. Очевидно что в зависимости от рыночных условий и возникающих задач, эта плата может оказаться значимой. Другие аспекты сравнения С# и С++, можно найти в моей предыдущей статье Выбор между C++ и C#.
Комментарии (211)
VEG
06.09.2015 20:13+2Верно, в C# при каждом обращении по индексу происходит проверка за пределы массива. Но этого можно избежать с использованием небезопасного кода на C# — в таком случае результат должен быть идентичен производительности кода на C++, где также нет проверок вхождения значения в диапазон при каждом обращении к массиву.
Это конечно не значит, что всюду стоит использовать небезопасный код. Но для каких-то критичных участков кода это может быть полезным.X_OSL
06.09.2015 20:27+1В тесте, я попробовал просто обнести сортировку блоком unsafe {… }, и этого не хватило чтобы убрать проверки, вероятно без фиксирования указателей (как и указано в вашем примере) данный метод не работает.
После же того как я зафиксировал items проверки ушли и код items[j] = items[i]; скомпилировался без дополнительных call-ов (таких как в статье), однако же его все-равно получилось многовато кода.
items[j] = items[i];
00000105 mov eax,dword ptr [ebp-34h]
00000108 mov dword ptr [ebp-6Ch],eax
0000010b mov eax,dword ptr [ebp-6Ch]
0000010e mov edx,dword ptr [ebp-44h]
00000111 mov ecx,dword ptr [ebp-34h]
00000114 mov dword ptr [ebp-70h],ecx
00000117 mov ecx,dword ptr [ebp-70h]
0000011a mov ebx,dword ptr [ebp-40h]
0000011d mov ecx,dword ptr [ecx+ebx*4]
00000120 mov dword ptr [eax+edx*4],ecxX_OSL
07.09.2015 01:42+2Все-таки, при правильном получении disassembly и использовании unsafe\fixed код получился такой для items[j] = items[i];
items[j] = items[i];
000000a1 mov ebx,dword ptr [ebp-38h]
000000a4 mov esi,dword ptr [ebp-38h]
000000a7 mov eax,dword ptr [esi+edi*4]
000000aa mov dword ptr [ebx+ecx*4],eax
monah_tuk
07.09.2015 01:38Не для raw массивов, но для array и vector: можно поиграться с at() и operator[] — оба делают одно дело, но: первый проверяет границу и бросает исключение, второй — нет, что бы полностью соответствовать семантике своего raw-собрата и не давать просадки в производительности, где они не ожидаются.
Mrrl
07.09.2015 11:58+1Переход на fixed увеличил время работы примерно в 1.6 раза — с 5.7 сек до 9.6 (на 100 сортировок). VS 2013, .NET 4, Any CPU
Mrrl
07.09.2015 12:48+1Код в safe mode (тело внутреннего цикла):
014B04D4 mov eax,dword ptr [ebp-3Ch] ;; [ebp-3Ch] = items 014B04D7 mov ebx,dword ptr [eax+4] if(items[i]<items[j]) { 014B04DA mov eax,dword ptr [ebp-1Ch] ;; [ebp-1Ch] = i 014B04DD mov edx,dword ptr [ebp-3Ch] 014B04E0 cmp eax,ebx 014B04E2 jae 014B0574 ;; exception? 014B04E8 mov esi,dword ptr [edx+eax*4+8] ;; items[i] 014B04EC mov eax,dword ptr [ebp-3Ch] 014B04EF cmp ecx,ebx ;; ecx = j 014B04F1 jae 014B0574 014B04F7 mov edi,dword ptr [eax+ecx*4+8] ;; items[j] 014B04FB cmp esi,edi 014B04FD jge 014B0510 014B04FF mov eax,dword ptr [ebp-1Ch] if(items[i]<items[j]) { 014B0502 mov edx,dword ptr [ebp-3Ch] 014B0505 mov dword ptr [edx+eax*4+8],edi ;; items[i]= saved items[j] items[j]=tmp; 014B0509 mov eax,dword ptr [ebp-3Ch] 014B050C mov dword ptr [eax+ecx*4+8],esi ;; items[j] = saved items[i]
Код для unsafe mode (fixed int *I=items):
if(I[i]<I[j]) { 015204DF mov edx,dword ptr [ebp-18h] ;; [ebp-18h] = I 015204E2 mov eax,dword ptr [edx+ebx*4] ;; ebx = i 015204E5 mov edi,dword ptr [ebp-18h] 015204E8 cmp eax,dword ptr [edi+esi*4] ;; esi = j 015204EB jge 0152050B tmp=I[i]; 015204ED mov edx,dword ptr [ebp-18h] 015204F0 mov eax,dword ptr [edx+ebx*4] 015204F3 mov dword ptr [ebp-20h],eax I[i]=I[j]; 015204F6 mov ecx,dword ptr [ebp-18h] 015204F9 mov edi,dword ptr [ebp-18h] I[i]=I[j]; 015204FC mov eax,dword ptr [edi+esi*4] 015204FF mov dword ptr [ecx+ebx*4],eax I[j]=tmp; 01520502 mov edi,dword ptr [ebp-18h] 01520505 mov eax,dword ptr [ebp-20h] 01520508 mov dword ptr [edi+esi*4],eax
Видно, что компилятор не хочет даже оптимизировать два обращения к I[i].
Поможем ему: перепишем внутренний цикл в unsafe mode так:
int a=I[i],b=I[j]; if(a<b) { I[i]=b; I[j]=a; }
Получилось заметно лучше:
int a=I[i],b=I[j]; 021D04DF mov edx,dword ptr [ebp-18h] 021D04E2 mov ecx,dword ptr [edx+edi*4] 021D04E5 mov edx,dword ptr [ebp-18h] 021D04E8 mov edx,dword ptr [edx+esi*4] if(a<b) { 021D04EB cmp ecx,edx 021D04ED jge 021D04FB I[i]=b; 021D04EF mov ebx,dword ptr [ebp-18h] 021D04F2 mov dword ptr [ebx+edi*4],edx I[j]=a; 021D04F5 mov edx,dword ptr [ebp-18h] 021D04F8 mov dword ptr [edx+esi*4],ecx
Интересно, почему он каждый раз достаёт I.
Времена (для x86):
safe mode: 7.5 sec
unsafe, first variant: 10.46 sec
unsafe, second variant: 5.95 secX_OSL
07.09.2015 12:51Интересно, — спасибо. Unsafe оптимизация не очевидная конечно. Попробую добавить в тест такой вариант.
X_OSL
07.09.2015 13:03У меня под VS2015 x64 unsafe реализация работает медленнее safe, даже с учетом оптимизаций
Код в тесте такой:
fixed (int* items = itemsi)
{
for (int i = 0; i < ITEMS_COUNT; i++)
items[i] = i;
int a, b;
for (int i = 0; i < ITEMS_COUNT; i++)
for (int j = i; j < ITEMS_COUNT; j++)
{
a = items[i];
b = items[j];
if (a < b)
{
items[j] = a;
items[i] = b;
}
}
}
Что я делаю не так?..
lair
07.09.2015 12:54Интересно, почему он каждый раз достаёт I.
(предположение) потому что раз уж вы полезли в unsafe, вы лучше знаете, чего вы хотите, и не надо вам мешать?Mrrl
07.09.2015 13:04Я тоже так думаю. Но как добиться от него помощи — чтобы и индексы не проверял, и указатель доставал один раз, и не нужно было подключать unmanaged код, а можно было оставаться в C#?
lair
07.09.2015 13:06По-моему, для «отключения» проверки индексов достаточно сравнивать с
array.Length
. Ниже это обсуждается.Mrrl
07.09.2015 13:14Да, 4.3 сек (вместо 7.5). Вполне нормально — там, где это работает (где индексы просто перебираются, а не достаются из других таблиц).
Mrrl
07.09.2015 16:07Если внимательно посмотреть тот код, то можно увидеть, что одна проверка индекса там осталась:
01322E1B mov eax,dword ptr [ebp-14h] 01322E1E cmp ebx,eax 01322E20 jae 01322E46
Возможно, проверки отключаются только для цикла от 0 до array.Length, а если начинать с другого значения, то остаются.
Mrrl
07.09.2015 13:11Итак:
fixed(int* I=items) { for(int k=0;k<100;k++) { int* A=I; for(int i=0;i<N;i++) A[i]=i; int tmp; for(int i=0;i<N;i++) { for(int j=i;j<N;j++) { if(A[i]<A[j]) { tmp=A[i]; A[i]=A[j]; A[j]=tmp; } } } } }
Внутренний цикл такой:
if(A[i]<A[j]) { 010404DF mov edi,dword ptr [edx+ecx*4] 010404E2 mov esi,dword ptr [edx+ebx*4] 010404E5 cmp edi,esi 010404E7 jge 010404EF A[i]=A[j]; 010404E9 mov dword ptr [edx+ecx*4],esi A[j]=tmp; 010404EC mov dword ptr [edx+ebx*4],edi
Переменную A он держит в edx, индексы — в ebx и ecx.
4.2 сек. Многовато, но лучше, чем первые варианты.
Кто бы мог подумать, что fixed указатели надо дублировать :)
Mrrl
07.09.2015 13:01+1Переписал через указатели:
int* E=I+N; for(int* p=I;p!=E;p++) { for(int* q=p;q!=E;q++) { if(*p<*q) { tmp=*p; *p=*q; *q=tmp; } } }
Получилось:
if(*p<*q) { 00C104EC mov edx,dword ptr [edi] 00C104EE mov eax,dword ptr [esi] 00C104F0 cmp edx,eax 00C104F2 jge 00C104F8 *p=*q; 00C104F4 mov dword ptr [edi],eax *q=tmp; 00C104F6 mov dword ptr [esi],edx for(int* q=p;q!=E;q++) {
3.67 сек. Это уже неотличимо по скорости от C++.
Но здесь надо быть осторожным: одно неловкое движение — и оптимизация летит к чертям. Например, попытка обратиться к q[1] может стоить очень много.X_OSL
07.09.2015 13:13+1Даже для С++ разработчика данный код выглядит жестко.
Но у меня в тесте он показал производительность примерно(а рамках погрешности измерений) равную производительности С++ кода не переписанного подобным образом.
В целом это выход, если очень нужна производительность, но конечно очень не простой для среднестатистического С# разработчика.
Dywar
08.09.2015 11:01+1Если бы это было так просто, например:
«Существуют особые ситуации, когда JIT-компилятор может отключить проверку границ при обращении к элементам массива — в цикле for, выполняющем обход всех элементов.»
И unsafe код здесь не нужен.
// Проверка границ отсутствует for (int k = 0; k < array.Length - 1; ++k) { array[k] = (uint)k; } // Проверка границ отсутствует for (int к = 7; к < array.Length; ++к) { array[k] = (uint)k; } // Проверка границ отсутствует // JIT-компилятор удалит -1 из проверки границ и начнет со второго элемента for (int k = 0; k < array.Length - 1; ++k) { array[k + 1] = (uint)k; } // Проверка границ выполняется for (int k = 0; k < array.Length / 2; ++k) { array[k * 2J = (uint)k; } // Проверка границ выполняется staticArray = array; // "staticArray" - это статическое поле вмещающего класса for (int k = 0; k < staticArray.Length; ++k) ( staticArray[k] = (uint)k; }
Информацию об отключении проверки границ и некоторых особых случаях можно найти в статье «Array Bounds Check Elimination in the CLR» Дейва Детлефса (Dave Detlefs).
Источник — Голдштейн С. — Оптимизация приложений на платформе .NET — 2014.
AtomKrieg
06.09.2015 20:44+7Давайте напишем 10 строчек абстрактного кода и сделаем далеко идущие выводы; обоснуем выводы таблицами и графиками.
ПодбробнееКакое-то однобокое сравнение. Постараюсь объяснить свою точку зрения.
Во-первых, вы из замеров 10 строчек пытаетесь делать выводы.
Во-вторых, мы знаем что C++ выбирают для числодробилок, а C# это энтерпрайз. Возможно стоит взять несколько пар средних по размеру текста алгоритмов. Например, физические вычисления и парсинг большого xml. А потом сделать выводы:
Вывод 1: если полениться и реализовать числодробилку на С#, то получится такой-то штраф по скорости
Вывод 2: если напрячься и реализовать энтерпрайз на C++, то мы получим такие-то ускоренияX_OSL
06.09.2015 21:06+7Я хотел разобрать максимально простой пример, который при этом будет легко читаться в ассемблере. Мне кажется сложное складывается из простого.
Проанализировать более сложный алгоритм в ассемблере разумеется тоже реально, но его реализация на каждом из языков будет вызывать много спорных моментов (почему использована именно эта библиотека, почему использован именно этот тип, этот вид оптимизации и т.п.)
Ведь даже в моем примитивном примере пришлось рассмотреть альтернативные контейнеры и некоторые элементы оптимизации. В более сложном алгоритме, количество подобных вариаций думаю должно быть существенно больше, чтобы выполнить более менее объективное сравнения, это требует многократно больше времени.
Мне кажется анализировать элементы сложного проще, чем сложное целиком.
И кстати, попытка анализа чуть более сложных алгоритмов есть в статье Head-to-head benchmark: C++ vs .NET, на которую я ссылаюсь в своей статье.AtomKrieg
06.09.2015 21:59+7Если разбираете максимально простой пример, то вывод можно сделать только один — «в максимально простых примерах одно лучше другого на 10%-80%». Всё.
Еще раз: на основании ваших измерений нельзя делать больших выводов. В противном случае получится как в картинке «my hobby extrapolating»…X_OSL
06.09.2015 22:19-1Но я думаю, что вы согласитесь, что даже самые сложные решения состоят из максимально простых элементов.
В разобранном примере продемонстрированы издержки на managed среду (в частности, дополнительные проверки), и думаю даже в самом сложном примере задача обеспечения управления кодом никуда не уйдет.
Поэтому мне кажется экстраполировать само наличие издержек на «менеджмент» кода вполне можно, другое дело что возможно процент кода, необходимого для осуществления «менеджмента» в различных случаях будет разный.AtomKrieg
06.09.2015 22:26+2Но я думаю, что вы согласитесь, что даже самые сложные решения состоят из максимально простых элементов.
Это манипуляция чистой воды. С вами не получится построить конструктивный диалог, поэтому прекращаю общение.X_OSL
06.09.2015 22:33Прошу прощения, если форма моего высказывания показалась вам манипулятивной.
Но ведь я говорю чистую правду. Сложное состоит из простого, по крайней мере в разработке ПО.creker
06.09.2015 22:47Сложное состоит из простого, по крайней мере в разработке ПО.
Это — правда. А ваша экстраполяция это манипулирование с целью оправдать вашу известную предвзятость на счет темы managed и unmanaged языков.X_OSL
06.09.2015 23:02-1Моя экстраполяция это не манипулирование.
Считаете ли вы что менеджмент кода возможно осуществлять без накладных расходов? Если нет, то в чем я не прав, экстраполируя факт наличия расходов на менеджмент простого кода на более сложные случаи?lair
06.09.2015 23:18+1Если нет, то в чем я не прав, экстраполируя факт наличия расходов на менеджмент простого кода на более сложные случаи?
Очевидно, в соотношении этих расходов к общему времени выполнения кода; иначе говоря — в количественной оценке потери производительности.X_OSL
07.09.2015 00:54+1Я даже специально раздел в статье написал «Какая же все-таки реальная оценка?», где указал на это соотношение в реальных задачах.
lair
07.09.2015 00:59+2И там не написано ни одной конкретной цифры.
Возьмем, скажем, типовую такую энтерпрайз-задачу — у меня есть веб-сервис, принимающий SOAP-сообщения мегабайт в десять-пятнадцать на сообщение; дальше эти сообщения разбираются, протоколируются в БД, трансформируются и маршрутизируются дальше на следующий сервис.
Какова будет разница в производительности этого сервиса на среднестатистическом серверном оборудовании при его реализации на C++ и C#?X_OSL
07.09.2015 01:01+1Конкретная цифра будет разной в каждой конкретной задаче. Вы должны знать сколько процентов runtime приходится на ваш сервис, например померив этот процент профайлером и далее делать выводы о необходимости оптимизации.
lair
07.09.2015 01:05Ага, и откуда мы будем знать, сколько процентов приходится на сервис до его написания? Получается, что ваши тесты не дают никакой дополнительной информации для принятия решения, на чем писать ту или иную систему.
Я поэтому и говорю: нет ни одного способа формально масштабировать результаты ваших тестов на реальные задачи.X_OSL
07.09.2015 01:13+1Тесты измеряют лишь скорость выполнения «вашего» кода.
Сколько же времени придется на «ваш» код, а сколько на код системы вы должны определить профайлером или же вывести из собственного опыта в реализации или профилировании подобных задач.
Масштабировать результаты возможно, используя соотношение между временем выполнения вашего кода и кода системы. Соотношение нужно знать априорно, из опыта разработки.lair
07.09.2015 01:17Предположим, из общего времени выполнения соотношение между «моим» кодом и «системным» — 50/50. Отдельно заметим, что загрузка CPU выше 40% не поднимается все время работы.
И как результаты ваших тестов масштабируются на время работы моего сервиса?X_OSL
07.09.2015 01:26-1Тогда можно предположить, что использование managed кода даст дополнительные потери производительности в диапазоне 5-40%, и если загрузка CPU лишь 40%, то порядка 40% от этого диапазона, то есть ориентировочные потери будут лишь 2-16%
(если правда 40% загрузка)lair
07.09.2015 01:27+3А откуда вы берете цифры «дополнительных потерь» 5-40%?
X_OSL
07.09.2015 01:29-6Промасштабировал 10-80%, полученные в моих, и не только моих тестах.
lair
07.09.2015 01:31+1Но почему вы считаете, что разница в производительности c# и c++ на этой задаче будет составлять те же 10-80%?
(Не говоря уже о том, что само по себе измерение, дающее восьмикратный разброс показаний — это печаль)X_OSL
07.09.2015 01:46Различные тесты(не только мои) подтверждают примерно такую разницу.
По скольку тестов опровергающих данную разницу нет, считаю оправданным ее использование.
И разумеется восьмикратный разброс вполне объясним т.к. зависит от задачи.lair
07.09.2015 01:53В этих тестах использовался тот же код (хотя бы родственный), что в моей задаче? Хотя бы примитивы использовались те же?
X_OSL
07.09.2015 01:59Думаю общие принципы генерации managed кода в тестах схожи с вашим случаем. Думаю что принципы применяются схожим образом для разных типов. Хотя конечно в каждом случае могут быть свои нюансы.
Но управление кодом вряд-ли может быть бесплатным.lair
07.09.2015 02:02+1«Общие принципы» — это бессмысленные слова, они не дают никаких конкретных цифр, как следствие — не позволяют оценить конкретный выигрыш/проигрыш.
Управление кодом, конечно, не может быть бесплатным, но вопрос конкретных затрат на него.X_OSL
07.09.2015 02:05Я предложил вариант их оценки (вернее и задолго до меня такие варианты были предложены)
Какой вариант предложили бы вы?lair
07.09.2015 02:09+1Есть ровно один вариант оценки производительности применительно к конкретной задаче — реализация этой задачи и сравнение производительности реализаций. После накопления достаточного количества «типовых» задач можно будт делать какие-то количественные оценки.
(Заметим, качественная оценка — «скорее всего, оптимизированный c++ будет быстрее оптимизированного c#» — всем интуитивно понятна, только толку с нее немного в практическом применении)
Joshua
07.09.2015 17:30+1Троллинг чистой воды.
1. До разработки двух версий узнать оценку разницы производительности нельзя, имея на руках любые синтетические тесты.
2. Наличие синтетических тестов позволяет сделать хоть какую то оценку, а вопрос ее достаточности и достоверности каждый принимает сам.
Соответственно, Вы задаете вопрос: с чего 80%?
Вам отвечают: ну вот у меня тесты, я ОЦЕНИВАЮ что будет ПРИМЕРНО так же.
Вы: а что мне эти тесты, они вообще не про мою системуlair
07.09.2015 17:35+1Вот я и пытаюсь понять степень достоверности оценки производительности веб-сервиса, сделанной на основании замеров скорости сортировки в памяти. Пока что выходит, что она где-то на уровне случайной величины.
VenomBlood
07.09.2015 01:40+4Картинка была такая у xkcd:
мое хобби - экстраполяцияX_OSL
07.09.2015 01:48Я же все это описал. Нужно просто знать сколько времeни занимает ваш код в runtime.
VenomBlood
07.09.2015 01:57И это не всегда показатель. Если это какое-нибудь пользовательское приложение то пользователю без разницы занимаете оно 5% CPU или 25%, а уже тем более если это проценты одного ядра. Если вы пишете чат и время парсинга ответа сократили с 5мс до 1мс на референсной системе — толку от этого ноль, пользователь на глаз не отличит. Ну и кроме того даже если у вас процессор загружен по максимуму и оптимизаци позволят сэкономить на количестве серверов (или эквиваленте) — возможно дешевле будет добавить оперативки или докупить этих серверов. Задач где действительно так важна разница в производительности замеренных порядков (т.е. это же не 10 и не 100 раз) — мало, да и в них эти синтетические «80%» будут скорее всего далеки от истины. Кроме того сравнили вы два конкретных компилятора. Сегодня разница может быть X, а завтра выйдет новый компилятор и разница уже Y. А для C++ еще есть компиялатор от Intel — можно сразу с ним сравнивать.
creker
07.09.2015 01:47Теперь вопрос, а откуда вы знаете, что эти 40% времени делает процессор? Алгоритм может быть таков, что постоянно генерирует cache miss и в managed, и в unmanaged реализации. Большую часть времени процессор простаивает, ожидая данных, и, допустим, грузит процессор на 100%. Разве не можем мы здесь получить меньше 5% потерь? Ваша методика как-то учитывает это?
X_OSL
07.09.2015 01:51Было бы интересно, если бы вы привели пример такого теста, мы бы сравнили результаты выполнения managed и unmanaged реализации и сделали бы выводы.
creker
07.09.2015 02:01+6Т.е. ваша методика не учитывает банальнейшей ситуации неэффективной работы с памятью, отчего ее нельзя экстраполировать даже на другую небольшую задачку перемалывания чисел, в чем так хорош C++, не говоря уже про enterprise решения. Вам нужно модифицировать вашу оценку с 5-80% на 0% до +?, тогда она будет более верной, но, к сожалению, все такой же бесполезной.
X_OSL
07.09.2015 02:03-6Опять таки, приведите примеры тестов, которые бы учитывали неэффективную работы с памятью, думаю всем будет интересно почитать статью про это.
creker
07.09.2015 03:32Ради любопытства я все таки тест сделал. Код — банальнейший пример cache miss, который на каждой лекции по теме показывают. Заполнение двумерного массива 10000x100000 вложенными циклами. Перестановка циклов — cache miss пропадают и время выполнения уменьшается на порядки. Я сделал даже сложнее вариант — заполнение элементами из противоположного конца массива, чтоб побольше промахов было. Невероятным образом получил на 2012 студии и .Net 4.5.1 результат такой, что C# стабильно быстрее на 40-50 миллисекунд в масштабах 30 секунд общего времени. Статью я из этого по понятным причинам делать не буду, результат забавный, не более.
X_OSL
07.09.2015 07:18Пожалуйста, выложите хотя бы исходники этого сравнения, очень интересно посмотреть как именно реализованы C++ и С# варианты.
VenomBlood
07.09.2015 22:20+1Ответили же:
Заполнение двумерного массива 10000x100000 вложенными циклами.
Или вы вообще не в курсе что такое cache miss? Ну и зачем тогда писать статью о «производительности» если даже базовые вещи надо разжевывать?X_OSL
07.09.2015 22:29-2Было бы неплохо увидеть точную реализацию (хотя общая идея и понятна)
VenomBlood
07.09.2015 22:31Если вы не знаете как работает подсистема памяти и кэш в частности — так и скажите. Что может быть проще двух вложенных циклов заполняющих двумерный массив? Ну зайдите в гугл чтоли посмотрите.
X_OSL
07.09.2015 22:38-2Вопрос итератор какого из циклов за какой индекс массива будет отвечать и наличия другого кода.
cache miss, по идее должен возникать когда будет обращение к элементам массива находящимся друг от друга на расстоянии больше, чем размер кэша. То есть вложенным должен поидее быть итератор, который отвечает за итерацию по «колонкам»(с точки зрения расположения в памяти) двухмерного массива.
И другой вопрос в том как именно данный пример сравнивает С++ и C#?
Кто-то из них способен избежать эффекта cache miss?VenomBlood
07.09.2015 22:42+1Вопрос итератор какого из циклов за какой индекс массива будет отвечать и наличия другого кода.
А что, есть варианты?
cache miss, по идее должен возникать когда будет обращение к элементам массива находящимся друг от друга на расстоянии больше, чем размер кэша.
Что и подтверждает мои слова о том что вы не в курсе как работает кэш. Почитайте хотябы вики на досуге, кэш работает по линиям. Нет, ну конечно если «больше чем размер кэша» — тоже miss будет, но условие совершенно не обязательное.
И другой вопрос в том как именно данный пример сравнивает С++ и C#?
А вы выше читайте что вам сказали, а то уже забыли на что отвечали.
Кто-то из них способен избежать эффекта cache miss?X_OSL
08.09.2015 09:11-1Я несколько раз прочитал все написанное выше, и не увидел ответа на вопрос:
Почему эффект cache miss будет разным для С++ и С# при работе с данными одинакового вида и использовании одинакового алгоритма обработки?
creker
07.09.2015 00:17+3До тех пор, пока вы не конкретизируете эти сложные случаи, ваши слова останутся манипулированием. А после станут голословными утверждениями, потому что оценка расходов требуется в каждом конкретном случае. В этом вы не правы. Ваша статья не имеет абсолютно никакого смысла, кроме как — пузырьковая сортировка работает на С++ быстрее при конкретных условиях. Больше никуда эту статью экстраполировать невозможно.
X_OSL
07.09.2015 00:56А какую оценку оценку производительности или методику, которую можно экстраполировать на сложные случаи по вашему мнению?
KvanTTT
06.09.2015 22:33+7По началу статьи догадывался, что написал ее автор и этой статьи: Выбор между C++ и C#. Открыл профиль, и мои догадки подтвердились. Почитайте комментарии к ней.
X_OSL
06.09.2015 22:44В этой статье дана прямая ссылка на упомянутую вами статью. И разумеется я читал все комментарии к ней и на большую часть из них отвечал.
Из комментариев я понял, что ряд из моих утверждений в статье про выбор выглядели крайне голословно, и это упущение я понемногу пытаюсь исправить.
Door
06.09.2015 21:15+1Ну, как по мне, смысла нет. И так ясно: если стремиться написать код, который должен очень быстро работать, то качественное решение на C++ выиграет у качественного решения на C#. Другой вопрос в том, какие затраты на написания качественного решения: для C++ они выше. Как по мне, если брать среднестатистического программиста, то C# выигрывает в плане быстроты и удобства разработки, что вполне окупает потерю производительности.
AtomKrieg
06.09.2015 22:06Тут без калькулятора не обойтись. Ожидаемая выгода = ожидаемая экономия на зарплатах — ожидаемые дополнительные затраты (например, электроэнергии) за весь период эксплуатации ПО. Составляете сметы и вперед.
Для простого программы не имеет смысла. Для сложного решения замеры в статье ничего не дадут. Потому, что нельзя просто так взять и экстраполировать пузырьковую сортировку на энтерпрайз решение.
Athari
07.09.2015 03:39-2Как по мне, если брать среднестатистического программиста, то C# выигрывает в плане быстроты и удобства разработки, что вполне окупает потерю производительности.
Есть ещё один нюанс: если посадить нуба писать на плюсах, то он напишет глючное тормозное поделие. Если этого же нуба посадить писать на шарпе, то с большой вероятностью код будет работать быстрее. Это происходит по многим причинам: нуб будет воевать с плюсами, а не оптимизировать; «классы по умолчанию» в дотнете подобраны лучше и так далее. То же верно не только для нубов, но и для хороших специалистов, которых менеджеры подгоняют писать быстрее.
Так как в энтерпрайзе количество гениев ограничено, а крайний срок всегда «вчера», то внезапно оказывается, что на C# не только быстрее разрабатывать, но и сам код будет производительнее. Просто энтерпрайз такой энтерпрайз.
А ещё энтерпрайзу часто гораздо критичнее падение программы один раз, чем увеличение цены на железо. Энтерпрайз любит стабильность во всём. И тут опять с плюсами не по пути.
Zibx
07.09.2015 03:54+1Тут было бы интересно притянуть rust, go, D, nim и вот это всё новое и чудесное и посмотреть что генерирует оно, пусть даже на вот этой вот задачи. Из результатов нормальных выводов опять же сделать было бы нельзя, но всё равно любопытно.
dyadyaSerezha
07.09.2015 16:41+2Абсолютно согласен. Заголовок — сравнение производительности, на самом же деле сравниваются операции доступа по индексу. Ясень пень, что встроенная проверка на границы массива добавит свой вклад.
Не сравнивается:
1. Реальная итерация по коллекциям,
2. Вообще работа со сложными коллекциями, хотя в 90% кода используются именно такие коллекции (те же хеш-таблицы, словари и очереди), а не простые массивы.
3. Реальная работа с памятью; то, что в примере, это просто смешно — размещение ОДНОГО объекта и его удаление. При реальном создании/удалении сотен тысяч и миллионов объектов разного размера, в случае с С++ это может может быть нетривиальный поиск/выбор свободного слота в куче и возвращение в кучу; в случае с С# это задержки на собирание мусора и сжатие кучи.
4. Даже в этом тривиальном случае GC.Collect скорее всего не удалит массив, потому что он больше 8К, что является порогом для создания объектов в отдельной куче (LOH, Large Object Heap), которая не чистится по умолчанию. Но повторюсь, тривиальный случай — не случай вообще.
5. Почему выбран int как элемент массива? А если для С++ выбрать указатели и размещать/удалять элементы (соотв. сделать reference type для C#). Почему бы не добавить переписанную операцию сравнения для элементов, как это часто происходит в реальной жизни?
Но главное сказано в начале: сравнивается банально скорость доступа по индеску. Всё.
В общем тест напоминает сравнение лука и револьвера, если и тот и другой закрепить в полуметре от мишени: результат будет 100% попаданий в обоих случаях. Я вовсе не хочу С++ или С# объявить луком. Я показываю неправомерность вот таких «стендовых» испытаний.
Но даже если сравнивать этот конкретный тривиальный случай, я бы попробовал переписать С# вариант как-то так:
....
uint i = 0; j = 0; foreach (int itemI in items) { foreach(int imemJ in items) { if (itemI < itemJ) { items[j] = itemI; items[i] = itemJ } ++j; } ++i; }
X_OSL
07.09.2015 17:06Приведенный вами код, не выглядит рабочим и как я понимаю, он должен увеличить j свыше размерности массива а потом скорее всего свалиться с выходом за рамки массива.
Да и выложенный цикл у вас должен быть не полным перебором, а перебором начиная с i…Mrrl
07.09.2015 17:30Кроме того, после операции items[i]=itemsJ должно поменяться значение, которое в этой программе лежит в itemI (а в оригинале — к нему всегда обращаются как к items[i]).
dyadyaSerezha
07.09.2015 21:12Блин, ну быстро написал я кусок кода, забыл вставить присваивание j нулю перед внутренним циклом. Идею-то вы поняли или нет?
Mrrl
07.09.2015 21:19Я не понял. Как с помощью foreach начать цикл с середины? И какой паттерн для массива правильнее — foreach и счётчик, или for и индекс? Скорее всего, foreach раскроется в тот же for + взятие элемента по индексу.
dyadyaSerezha
09.09.2015 18:42Да, я быстро глянул в исходный код, не заметив, что внутренний цикл не с нуля, а с i. Но хотя бы внешний for можно поменять на foreach. Идея была такая, что внутренняя реализация foreach должна гарантировать невыход за границы массива, поэтому должна отсутствовать проверка индекса на каждом шаге. Плюс, может быть оптимизирован доступ к очередному элементу массива (в ассемблере для for каждый раз тупо вычисляется с нуля сдвиг в байтах относительно начала массива и потом итоговый адрес). К сожалению, судя по комментам на других сайтах, для рантайма 3.5 включительно, мои предположения неверные. Что странно, вообще говоря. Неужели и в 4.5 ничего не поменялось?
X_OSL
07.09.2015 21:22Даже если добавить j=0, не ясно как
foreach(int imemJ in items)
может стать тождественным
for (int j = i; j < ITEMS_COUNT; j++)
пока идея не совсем понятна…
fshp
06.09.2015 20:58Объявление int tmp; за циклом в случае C# экономит время
Не уж то на стеке даже примитивы размещать не умеет?Leopotam
06.09.2015 22:49Зависит от рантайма. В разных версиях моно оно себя ведет по-разному. Поэтому вынесение переменных вне тела гарантирует единообразное поведение в плане производительности.
fshp
06.09.2015 23:22О mono в статье ни слова. Точнее есть упоминание, но к тестам это никак не относится.
Leopotam
06.09.2015 23:53Тогда это сферические тесты в вакууме, если ограничиваться исключительно одной операционкой и одной версией рантайма. Рантайм для MSIL может быть использован как кросс-платформенное решение и быть переносимым, если знать, что можно использовать, а что не рекомендуется. В комменте выше как раз указывается о разнице в производительности на разных версиях моно для вложенных в блок marshal-by-value типов. Так же есть эпичные фейлы из-за особенностей реализации, например, если попытаться использовать enum-тип в качестве ключа в Dictionary и делать перечисление или вообще любое обращение к данным — будет происходить boxing/unboxing у ключа с выделением и пометкой для GC памяти. Причем если использовать в качестве ключа int и кастовать enum к нему — все работает как на фреймворке от MS. Как сейчас под всякими mono 4.x и тп не знаю, проверялось на 2.8 и 3.0.
onto
06.09.2015 21:47+3А почему std::vector тестировался без reserve() если размер известен?
X_OSL
06.09.2015 22:10+2Спасибо. Да, действительно reserve был бы очень полезен в тесте для std::vector.
Я попробовал его добавить в тест, суммарные результаты получились схожими, и хотя создание вектора стало занимать чуть больше времени, зато заполнение вектора данными стало примерно в 2.5 раза быстрее.
Правда сортировка заняла столько же времени (что наверное логично), поэтому сумма изменилась не существенно.
Godless
06.09.2015 22:26-4а мне кажется, что получилось достаточно честно, насколько это вообще можно сравнить… 8)
А может я просто симпатизирую плюсикам))
xtraroman
06.09.2015 22:34+1Какую проблему вы хотите помочь решить? Выбрать c++ или c# до начала реализации проекта? Ну так этот выбор надо делать в зависимости от скилов разработчиков и требований заказчика. Если производительности managed кода окажется недостаточно, во-первых, можно его оптимизировать разными способами, во-вторых, можно реализовать «горячие» методы на c++ или даже на ассемблере в отдельной сборке. Т.е. вообще нет необходимости выбирать что то одно, как вы написали: c# или c++.
X_OSL
06.09.2015 22:38+2В статье я лишь пытаюсь оценить стоимость выбора между С# и С++ с точки зрения производительности. Разумеется, производительность — далеко не единственный критерий для выбора средства разработки, и поэтому, как правило, нельзя делать выбор основываясь исключительно на нем.
lair
06.09.2015 23:17+1пытаюсь оценить стоимость выбора между С# и С++ с точки зрения производительности.
А где методика оценки стоимости? И результаты этой оценки в конкретных цифрах?X_OSL
06.09.2015 23:22Стоимость оценивается исключительно в единицах времени, которое занимает код в runtime.
Методика заключается в измерении этого времени и анализе disassembly являющегося причиной разницы этого времени. (хотя методика, это наверное слишком громко сказано)lair
06.09.2015 23:27+1Стоимость оценивается исключительно в единицах времени, которое занимает код в runtime.
Тогда это не стоимость, потому что эти единицы времени ничего не значат. Стоимость (для проекта) — это доллары (и прочие деньги).
«Вы потеряете 10-80% производительности» (даже если ваши предположения верны) — это ни о чем. Ну потеряю. Что дальше? Как это повлияет на мой проект?X_OSL
06.09.2015 23:39+2Это более сложный вопрос. Он за рамками статьи. Есть некоторые соображения на этот счет, но пока они далеки от формализованных. Попробую как-нибудь написать об этом боле развернуто, в виде статьи, когда сформулирую.
lair
06.09.2015 23:42+3Что возвращает нас к вопросу из начала треда: какую проблему вы пытаетесь решить? Какой смысл мерять абстрактные проценты производительности на абстрактных простых задачах?
X_OSL
06.09.2015 23:56-1Я пытаюсь оценить издержки, простые задачи позволяют это сделать и позволяют выполнить анализ причин.
Далеко идущие выводы из одной этой оценки вряд-ли стоит делать, но думаю есть смысл принимать их во внимание при анализе среди прочих факторов.lair
06.09.2015 23:58Вам уже неоднократно объяснили, что простые задачи позволяют оценить издержки на простых задачах. Вопрос их применимости к сложным задачам остается открытым.
x512
06.09.2015 22:41+4По-хорошему это даже не сравнение языков C# vs C++, а сравнение оптимизирующих компиляторов. А если быть точным то сравнение традиционного и JIT компилятора. Посему непонятно, где указаны конкретные версии компиляторов и их настройки?
x512
06.09.2015 22:46ЗЫ: Есть только указание на .Net 4.5 64, а, как известно, 64 битная платформа была значительно оптимизирована в .Net 4.6
X_OSL
06.09.2015 22:49В статье я писал, что использовал Visual Studio 2010 и настройки по умолчанию для релизной конфигурации (исходники с настройками проекта приложены в статье)
И если быть более точным, то Visual Studio 2010 SP1, С++ компилятор 16.00.40219.01, С# компилятор 4.0.30319.17929.x512
06.09.2015 22:56+3Понятно, тогда было бы интересно узнать что изменилось за 5 лет и не сократился ли разрыв между ними?
X_OSL
06.09.2015 23:04Позже, когда дойдут до этого руки, попробую все перепроверить на 2015 студии.
x512
06.09.2015 23:07+1Скомпилил C# на 15 студии, на платформе 32. Как видите, все стало значительно лучше. Может быть, на 64 еще лучше будет.
for (int i = 0; i < items.Length; i++)
01322DFE xor ecx,ecx
for (int i = 0; i < items.Length; i++)
01322E00 mov eax,dword ptr [edi+4]
01322E03 mov dword ptr [ebp-10h],eax
01322E06 test eax,eax
01322E08 jle 01322E3E
for (int j = i; j < items.Length; j++)
01322E0A mov ebx,ecx
01322E0C cmp dword ptr [ebp-10h],ecx
01322E0F jle 01322E38
01322E11 mov eax,dword ptr [edi+4]
01322E14 mov dword ptr [ebp-14h],eax
{
if (items[i] > items[j])
01322E17 mov esi,dword ptr [edi+ecx*4+8]
01322E1B mov eax,dword ptr [ebp-14h]
01322E1E cmp ebx,eax
01322E20 jae 01322E46
01322E22 mov edx,dword ptr [edi+ebx*4+8]
01322E26 cmp esi,edx
01322E28 jle 01322E32
01322E2A mov dword ptr [edi+ebx*4+8],esi
items[i] = tmp;
01322E2E mov dword ptr [edi+ecx*4+8],edx
for (int j = i; j < items.Length; j++)
01322E32 inc ebx
01322E33 cmp dword ptr [ebp-10h],ebx
01322E36 jg 01322E17
for (int i = 0; i < items.Length; i++)
01322E38 inc ecx
01322E39 cmp dword ptr [ebp-10h],ecx
01322E3C jg 01322E0AX_OSL
06.09.2015 23:14Спасибо, а как у вас выглядит items[j] = items[i];?
Почему то не вижу в вашем примере кода для
items[j] = items[i];
items[i] = tmp;
а вижу только код для items[i] = tmp;x512
06.09.2015 23:22+1Тут вся соль оптимизирующего компилятора! Он в начале загнал сравниваемые числа в регистры esi и edx, сравнивает и двумя последними mov меняет их местами
01322E26 cmp esi,edx
01322E28 jle 01322E32
01322E2A mov dword ptr [edi+ebx*4+8],esi
items[i] = tmp;
01322E2E mov dword ptr [edi+ecx*4+8],edxX_OSL
06.09.2015 23:34Ага, понятно, спасибо.
И правда скомпилировал заметно лучше, действительно сильный прогресс в рассмотренном примере.
А проверка выхода за границы массива теперь ушла?
Так же, как понимаю, переменную tmp с оптимизировали до использования регистров?
Вообще конечно надо будет уже скоро переходить на 2015 студию.X_OSL
07.09.2015 00:59Обновление: дело было в неправильном получении disassembly.
На самом деле и в случае VS2010 оптимизация вполне приличная, но все-таки несколько хуже чем в случае с С++.
Тесты на 2015 студии показали разницу порядка 30% на платформе Core i7-3770
rPman
06.09.2015 22:53+1С удорожанием стоимости каждого нового % производительности просто чаще будут переходить на модульную разработку — критичные части кода (точнее приложения) писать на том, что легко оптимизировать (в т.ч. и за счет простого выбора языка программирования).
KvanTTT
06.09.2015 23:03+4Для этого возьмем код сортировки из самых быстрых примеров и посмотрим во что он компилируется, смотреть будем используя отладчик Visual Studio 2010 и режим disassembly, в результате для сортировки увидим следующий код:
Вы это серьезно? Вообще-то релизный код отличается от кода в режиме отладки.
Зачем вызывать сборщик мусора у дотнета? В реальных условиях он вызывается самостоятельно и редко в нескольких случаях: при превышении какого-то лимита памяти, из-за внешнего воздействия ОС и других. Т.е. искусственно его вызывать не честно.
А вы пробовали вместо 10000 использовать items.Length? В этом случае вроде диапазон выход за границы диапазона не проверяется.
А так вообще бессмысленное сравнение сферического кода в вакууме, из которого следует и так очевидный вывод: «Не используйте .NET в очень редких случаях для числодробилок».x512
06.09.2015 23:08А вы пробовали вместо 10000 использовать items.Length?
Я попробовал, ничего не поменялось в этом случае
X_OSL
06.09.2015 23:12-1Я привел disassembly релизного кода, а не дебажного.
Сборщик мусора вызывал для эмуляции delete (о чем писал в статье), ведь в реальном приложении рано или поздно будет потрачено время на сборку мусора, хотелось как то его учесть.
>вместо 10000 использовать items.Length?
Прямо сейчас попробовал. В результате items[j] = items[i]; скомпилировалось в
items[j] = items[i];
00000106 mov eax,dword ptr [ebp-38h]
00000109 mov edx,dword ptr [ebp-58h]
0000010c cmp eax,dword ptr [edx+4]
0000010f jb 00000116
00000111 call 73153D61
00000116 mov eax,dword ptr [edx+eax*4+8]
0000011a mov dword ptr [ebp-4Ch],eax
0000011d mov eax,dword ptr [ebp-3Ch]
00000120 mov edx,dword ptr [ebp-58h]
00000123 cmp eax,dword ptr [edx+4]
00000126 jb 0000012D
00000128 call 73153D61
0000012d mov ecx,dword ptr [ebp-4Ch]
00000130 mov dword ptr [edx+eax*4+8],ecx
На вид явно остались какие то проверки…withkittens
07.09.2015 00:08+2Я привел disassembly релизного кода, а не дебажного.
Как вы получили disassembly релизного кода? Распишите по шагам, пожалуйста. В .NET тут есть нюанс.X_OSL
07.09.2015 00:15Я скомпилировал релизный билд, поставил breakpoint в коде, запустил отладку,
когда выполнение остановилось на breakpoint, я перешел в disassembly из контекстного меню.
Если есть более правильный способ, распишите его пожалуйста тоже.withkittens
07.09.2015 00:18+5запустил отладку,
На этом шаге все JIT-оптимизации отключились.
Добавьте перед кодомConsole.ReadKey();
Запустите приложение без отладки и, пока оно сидит вReadKey()
, прицепитесь к процессу.
Тогда вы увидите оптимизированный код.X_OSL
07.09.2015 00:25+1Да, вы правы, тут моя ошибка, реальный оптимизированый код сортировки получается следующий
int tmp;
for (int i = 0; i < ITEMS_COUNT; i++)
00000076 xor edx,edx
00000078 mov dword ptr [ebp-38h],edx
for (int j = i; j < ITEMS_COUNT; j++)
0000007b mov ebx,dword ptr [ebp-38h]
0000007e cmp ebx,2710h
00000084 jge 000000BB
00000086 mov esi,dword ptr [edi+4]
{
if (items[i] < items[j])
00000089 mov eax,dword ptr [ebp-38h]
0000008c cmp eax,esi
0000008e jae 000001C2
00000094 mov edx,dword ptr [edi+eax*4+8]
00000098 cmp ebx,esi
0000009a jae 000001C2
000000a0 mov ecx,dword ptr [edi+ebx*4+8]
000000a4 cmp edx,ecx
000000a6 jge 000000B0
000000a8 mov dword ptr [edi+ebx*4+8],edx
items[i] = tmp;
000000ac mov dword ptr [edi+eax*4+8],ecx
for (int j = i; j < ITEMS_COUNT; j++)
000000b0 add ebx,1
000000b3 cmp ebx,2710h
000000b9 jl 00000089
for (int i = 0; i < ITEMS_COUNT; i++)
000000bb inc dword ptr [ebp-38h]
000000be cmp dword ptr [ebp-38h],2710h
000000c5 jl 0000007B
}
}
Спасибо, мне нужно обновитьX_OSL
07.09.2015 00:40Обновил код в статье и комментарий к нему.
Кстати, если использовать items.Length, то код по крайней мере для случая VS2010 получается примерно такой-же.
int tmp;
for (int i = 0; i < items.Length; i++)
0000007d xor ebx,ebx
0000007f mov eax,dword ptr [esi+4]
00000082 mov dword ptr [ebp-44h],eax
00000085 test eax,eax
00000087 jle 000000C3
for (int j = i; j < items.Length; j++)
00000089 mov edi,ebx
0000008b cmp dword ptr [ebp-44h],ebx
0000008e jle 000000BD
00000090 mov eax,dword ptr [esi+4]
00000093 mov dword ptr [ebp-48h],eax
{
if (items[i] < items[j])
00000096 mov edx,dword ptr [esi+ebx*4+8]
0000009a mov eax,dword ptr [ebp-48h]
0000009d cmp edi,eax
0000009f jae 000001BE
000000a5 mov ecx,dword ptr [esi+edi*4+8]
000000a9 cmp edx,ecx
000000ab jge 000000B5
000000ad mov dword ptr [esi+edi*4+8],edx
items[i] = tmp;
000000b1 mov dword ptr [esi+ebx*4+8],ecx
for (int j = i; j < items.Length; j++)
000000b5 add edi,1
000000b8 cmp dword ptr [ebp-44h],edi
000000bb jg 00000096
for (int i = 0; i < items.Length; i++)
000000bd inc ebx
000000be cmp dword ptr [ebp-44h],ebx
000000c1 jg 00000089
}
}
MaximChistov
06.09.2015 23:18Тестируйте в релиз режиме, этот ваш дебаг все портит
X_OSL
06.09.2015 23:24+1Я тестирую исключительно в режиме релиз, причем запуская скомпилированый exe отдельно от Visual Studio.
lair
06.09.2015 23:41+3Из тех, что попадались мне, самой содержательной показалась Head-to-head benchmark: C++ vs .NET
Четырехлетней давности? Серьезно?X_OSL
06.09.2015 23:43+1Буду рад если дадите ссылки на свежие сравнения, которые вам показались наиболее содержательными.
lair
06.09.2015 23:46Просто отдавайте себе отчет в том, что данные оттуда уже вряд ли актуальны.
X_OSL
07.09.2015 00:04Да, безусловно актуализация важна. Тесты актуальны для используемых версий Visual Studio и приведенного в них железа, но на свежей Visual Studio и например более свежем железе результаты могут быть другими.
encyclopedist
07.09.2015 00:45+2Автор этой статьи тоже использует старьё — Visual Studio 2010
X_OSL
07.09.2015 00:49Именно поэтому актуализирую результаты:
Собрал тесты под VS2015 и запустил.
Не вдаваясь в детали: самая быстрая сортировка для C++ заняла 153105, а самая быстрая для C# 206552.
То есть разница порядка 30%Ununtrium
07.09.2015 12:28Собрал тесты под VS2015 и запустил.
Что за каша? Как можно собирать под студию? С каких пор VS 2015 это платформа?
Вы что, под .NET 4.5 собирали с помощью VS 2015? В чем разница?X_OSL
07.09.2015 12:36Формулирую четче:
Собирал 2015 студией, C# компилятор 1.0.0.50618, C++ компилятор 19.00.23026 .Net Framework 4.6
Пробовал и х86 и х64 сборки, результаты получились схожими:
Самая быстрая C# реализация на 30% медленнее самой быстрой С++ реализации.Ununtrium
07.09.2015 13:34+1Ок, если вы ничего не меняли то под x64 используется RyuJit. Очевидно в данном тесте он выигрыша не дал.
А вообще я присоеденяюсь к тем, кто говорит что тест некорректный. Это даже не синтетический бенчмарк, а непойми что. Сравнивать надо типовые задачи.X_OSL
07.09.2015 13:37Не менял. Выигрыша за рамками погрешности измерений (т.е. за рамками ~3-5%) не было…
zim32
07.09.2015 03:15Недавно со знакомым мерялись с++ vs java в похожей задаче. Первый оказался примерно в 5 раз быстрее. Использовали стандартные алгоритмы и новые компиляторы. Эти языки для других задач писали.
MaximChistov
07.09.2015 03:20+3Первый оказался примерно в 5 раз быстрее
99% что джаву вы меряли криво, ну или не дали ей jit сделатьzim32
07.09.2015 10:30Я писал реализацию на с++. Не силен в java. Мы замеряли время сортировки, т, е, грубо говоря вызов одного метода. В джаве это Array.sort помоему. Нативные методы тоже на лету компилируются?
SOLON7
07.09.2015 06:21-8не удивительно, такие платфформы как java, .net добавляют большой оверхед, Тоесть как правило сравнивать разные инструменты для разных целей нецелесообразно! .net платформа очень много делает за разраба! за универсальность платформы и плюшки приходится платить, меньшей производельностью чем за компилируемые бинарники!
SOLON7
07.09.2015 06:28-7за программы уровня реального времени, приходится платить больше чем программерам которые пишут прикладное ПО, спасибо статья Гууд!!!
mapron
07.09.2015 07:31+4Уважаемый автор, посмотрите этот проект если будет интересно. Люди уже заморочились и сделали (куда менее синтетические) тесты, на алогоритмические задачи.
benchmarksgame.alioth.debian.org/u32/compare.php?lang=csharp&lang2=gpp
Да, статья довольно очевидная. C# никто и не выбирает в качестве «умопомрачительной числодробилки».X_OSL
07.09.2015 09:19Спасибо, интересная статья, хотя конечно 15-ти кратное превосходство С++ в тесте regex-dna выглядит немного странным, и версия Mono оставляет некоторые вопросы, но в целом результаты очень интересные, особенно учитывая возможность сравнения с другими языками.
FiresShadow
07.09.2015 08:10+1C# быстрее в одних ситуациях, а С++ — в других. Например есть кусок кода, который выполняется 2 раза. Меряем только 1й запуск — С++ быстрее за счёт того, что в С# работал JIT-компилятор. Меряем только второй запуск — C# быстрее за счёт того, что JIT-компилятор уже отработал и оптимизировал код под конкретную платформу. Меряем оба запуска — С++ быстрее за счёт того, что ускорение от оптимизации JIT-компилятора не покрыло расходы на работу JIT-компилятора. Меряем очень много запусков — C# быстрее за счёт того, что оптимизация JIT-компилятора окупилась и принесла дивиденды.
На серверных приложениях, которые перезапускаются раз в сутки или раз в месяц, C# быстрее (как минимум не медленнее). В «линейном» приложении С++ быстрее. В данной статье взяты примеры, которые совершенно не учитывают специфику работы JIT-компилятора, на основании чего сделаны спекулятивные выводы.
Ну и не следует забывать, что C# предоставляет возможности, которых нет в С++.
В некоторых случаях более уместен С (напр., микроконтроллеры), в других — C#, в третьих — javascript.FiresShadow
07.09.2015 10:14И, раз уж вы хотите принудительно вызывать сборку мусора через GC.Collect, которая тоже занимает время, то для чистоты эксперимента нужно и в примере на С реализовать менеджера умных ссылок.
Mogost
07.09.2015 08:27+3Для честности стоило бы делать разогрев, а не выполнять тест сразу с запуска на шарпе. В случае циклов шарп проседает в начале, а потом работает с скоростью C++, так как происходит кэширование инструкций.
И всё-таки gc.collect не эквивалент delete. Сборщик вынужден персчитать и просмотреть все ссылки, когда delete выполняет уже вашу инструкцию.X_OSL
07.09.2015 09:22Можете чуть подробнее описать (в идеале показать на disassembly), какие именно инструкции кэшируются в начале цикла С#? И как именно нужно прогревать С# код?
withkittens
07.09.2015 14:47+1какие именно инструкции кэшируются в начале цикла С#
Кэшируется всё тело метода.
При первом вызове метода JIT-компилятор компилирует MSIL в нативный код и кэширует его. При последующих вызовах метода исполняется этот нативный код.
Прогреть значит запустить как минимум один раз метод вне бенчмарка, чтобы он скомпилировался.
Либо скомпилировать сборку NGen'ом.
По слухам, для прогрева (в т.ч. на C++) нужно прогнать алгоритм десяток-два раз.
Что-то там про кэш процессора и т.д.
Тут в тонкостях бенчмаркинга я уже, увы, не силён.X_OSL
07.09.2015 15:00-2Понятно. Конкретно в моем тесте тогда получается прогревать не чего, объясню:
1. Весь код теста находится в одной функции, т.е. она будет уже скомпилирована
2. Третьи функции не вызываются из участков кода, подлежащих измерению.
Иными словами в тесте код уже «прогрет» к моменту начала измерений.
На счет прогрева с целью положить данные в кэш процессора — да понимаю о чем вы говорите. Но тут тесты С++ и С# находятся в равном положении — такой прогрев и там и там отсутствует.creker
07.09.2015 16:01+2Но тут тесты С++ и С# находятся в равном положении — такой прогрев и там и там отсутствует
Они не в равных условиях, потому что содержимое кеша в обоих тестах будет разным. Соответственно кеш-промахи будут происходит в различные моменты времени даже с идентичным кодом. Все таки ваш код не один на машине (ядре) исполняется и в кеше дофига чужих данных. Вы никогда не думали, почему время выполнения все время плавает? Именно поэтому. Начальные условия всегда разные, параллельно исполняемый код других приложений мешает. Поэтому делаются прогревы и усредняются показатели нескольких тестов.
Вам нужно изучать матчасть, если вы хотите что-то измерять. С такими рассуждениями даже нормальные тесты дадут цифры, которые ничего не значат.X_OSL
07.09.2015 16:13Я конечно же делал несколько тестов, в результате отклонения были до 3-5%. Думаю это отклонение и вызвано наличием промахов по кэшу, и прочих случайных событий. Мне кажется принять его за погрешность измерений достаточно для того чтобы считать результатами достоверными, но с погрешностью.
Также я думаю что промахи по кэшу и прочие условно случайных событий для С++ и для С# будут примерно одинаковыми.
sborisov
07.09.2015 10:29-5Nagg
07.09.2015 11:28Причем тут это к статье? Это просто очень криво написанный фраемворк, причем кривой код написан в том числе на плюсах.
sborisov
07.09.2015 11:43-1Это пример реальной программы, когда из «микро» складывается «макро». Хотелось бы примеров на код, чтобы оценить «кривоту»
Nagg
07.09.2015 11:46нет, это просто кривой код. почему он кривой — почитайте тут. С таким подходом хоть на плюсах, хоть на чем пиши — будет тупить.
creker
07.09.2015 12:22Только visual studio на этом же WPF работает отлично и ничем не выдает то, на чем она работает. Это просто кривой софт, написанный на фреймворке, с которым люди не умеют работать. В комментариях, к счастью, это упоминалось некоторыми.
X_OSL
07.09.2015 12:25-3Кстати WPF достаточно спорная по производительности библиотека, например в статье
Сравнение производительности UI в WPF, Qt, WinForms и FLTK
Я разбирал некоторые ее проблемы относительно работы с Datagrid.VenomBlood
07.09.2015 22:51Нет, вы разобрали производительность стандартного компонента с названием DataGrid в разных фреймворках. Общего было только название компонента, т.е. тест вообще лишен смысла.
X_OSL
08.09.2015 07:55-1Я разобрал производительность компонента с функциональностью DataGrid, это важное уточнение, так как при реализации практических задач нам важен функционал компонента.
Сравнение показало, что датагрид WPF, является одним из худших по производительности, причем на столько что при загрузке данными ентерпрайз уровня дает лишь 12 FPS на железе близком к топовому, а на железе по проще вообще еле шевелится.
Мне кажется знание этого очень полезно разработчикам, решившим использовать WPF датагрид для отображения тяжелых данных. Мне бы такое знание могло бы помочь года три-четыре назад, но к сожалению попадалась только реклама того, наскольо это хороший контрол…
Поэтому я и делюсь этим знанием, я хочу чтобы мир стал лучше либо за счет того что микрософт наконец таки оптимизирует свой контрол, либо за счет того что контролом не будут пользоваться там, где он не способен справляеться с задачей.
И мне кажется, что не я один хочу этого, потому что по результатам опроса, приведенного в статье, большинство ожидает хотя бы 30 FPS от приложений.VenomBlood
08.09.2015 07:58причем на столько что при загрузке данными ентерпрайз уровня
«Данные энтерпрайз уровня» вот любят же люди термины выдумывать. Нет никаких «данных энтерпрайз уровня», в enterprise может встречаться хоть 10 строк хоть 10 миллионов, границы нету.
Вы пишетеКстати WPF достаточно спорная по производительности библиотека, например в статье
Приводя в пример один стандартный контрол (у которого есть несколько сторонних альтернатив к тому же). Очень ценный совет, и очень ценное обобщение конечно же.X_OSL
08.09.2015 08:05Так или иначе количество строк и столбцов в статье написано точно и исходный код выложен.
Альтернативных гридов для WPF, из более менее зрелых библиотек к сожалению не нашлось. Те что попадались были либо достаточно сырыми opensource либо тормозили не меньше стандартного.
Если знаете быстрый грид для WPF, напишите пожалуйста, ну а если не знаете, то к чему ваш посыл?
stalkerg
07.09.2015 11:45Интересно посмотреть вызов функции с этим кодом… часто у языков узким местом является именно вызов функции.
Aclz
07.09.2015 14:55Код будет выполнять следующие действия:
1. Аллоцирование массива\контейнера
Что, простите?X_OSL
07.09.2015 15:01Выделять память под контейнер или массив.
Aclz
07.09.2015 15:25+1Это я-таки андерстуднул, но и вы, как вижу, можете выражаться по-русски, не имплементируя корреспондинговой комплементарщины.
X_OSL
07.09.2015 15:37Все-таки слово аллокация,- есть в русском языке, хотя и пришло в русский язык из других языков:
http://dic.academic.ru/dic.nsf/dic_fwords/3506/АЛЛОКАЦИЯ
http://dic.academic.ru/dic.nsf/business/605/Аллокация
Не знаю, возможно мне просто показался этот термин более лаконичным… я и правда часто использую его когда говорю о выделении памяти или других ресурсов…MacIn
07.09.2015 16:00+3хотя и пришло в русский язык из других языков:
Неважно, откуда пришло. Посмотрите сами ссылки — смысл слова совершенно иной. Как, например, «Признание правильности добавления к счету, последовавшего уже после подачи его» относится к выделению памяти?
Это примерно как в нелепом «мы продаем свою экспертизу». Слово-то «экспертиза» — есть, но употребляется в русском языке только в смысле «была проведена баллистическая экспертиза» и т.п.
allocate — назначать, выделять, распределять, отводить, размещать, располагать в определенном месте.X_OSL
07.09.2015 16:05Есть и толкование «Распределение продукции и производственных мощностей в пространстве рынка.», которое является ближе к выделению памяти.
Но в целом я соглашусь с вами, в русской языке этот термин чаще используется в несколько других целях.
maaGames
07.09.2015 16:35Не углядел ссылку на тестовые файлы…
Не понимаю, каким таким образом удалось получить такую разницу между std::vector и HeapArray. Если в векторе доступ был через оператор индексирования, то разницы с простым массивом быть в принципе не могло (мы же говорим о release версии). Вообще, между всеми четырьмя типами С++ массивов не должно было быть принципиальной разницы, учитывая размер массива. Такое чувство, что разница в результатах обусловлена кэш-миссами и вообще работой с памятью. Прям хочется повторить со всеми четырьмя массивами, но нужны исходники.X_OSL
07.09.2015 16:43-1В сылка в статье есть, разделе со словами «Остальные же примеры, со вставками для подсчета скорости выполнения,- полностью можно увидеть тут.»
http://www.filedropper.com/performance
Продублирую ссылку еще раз.
С вектором была разница в зависимости от платформы (на новых разницы не было). Возможно повлияли какие-то размеры кэша или же просто различные оптимизации в аппаратной реализации процессора…maaGames
07.09.2015 17:06+1Да, в актуальной версии разницы во времени доступа нет — всё ОК.
Очень мило среди пузырьков смотрится результат std::sort.
Mrrl
07.09.2015 17:43+3Попробовал сравнить производительность на функции из реального проекта (одна из самых «числодробительных» частей программы, на некоторых этапах работы в ней проходит 60% общего времени).
Функция выглядит примерно так:static int Unpack(int[] rec,int len,int[] res) { int p=0,np=0; int a=rec[p++],c=31; while(p<len) { for(int i=0;i<NCh;i++) { int t=a&1; int x=(t==0) ? LShort[i]+1 : 17; int r=a; if(x<c) { a>>=x; c-=x; } else { int s=x-c; a=rec[p++]; r|=a<<c; a>>=s; c=31-s; } x=32-x; r=(r<<x)>>(x+1); if(t==0) r+=Val[i]; Val[i]=r; res[np++]=UCvt[r&65535]; } } return np; }
(в массиве LShort лежат какие-то числа от 7 до 11, NCh=10).Mrrl
21.09.2015 19:21+1Сегодня я ещё немного поэкспериментировал с этой сортировкой.
Проверял 5 вариантов кода на C#void TestArray() { for(int i=0;i<L;i++) Arr1[i]=i; for(int i=0;i<Arr1.Length;i++) { for(int j=i+1;j<Arr1.Length;j++) { if(Arr1[i]<Arr1[j]) { int t=Arr1[i]; Arr1[i]=Arr1[j]; Arr1[j]=t; } } } } void TestArray2() { for(int i=0;i<Arr1.Length;i++) Arr1[i]=i; for(int i=0;i<Arr1.Length;i++) { for(int j=i+1;j<Arr1.Length;j++) { int a=Arr1[i],b=Arr1[j]; if(a<b) { Arr1[i]=b; Arr1[j]=a; } } } } void TestArray3() { for(int i=0;i<L;i++) Arr1[i]=i; for(int i=0;i<L;i++) { for(int j=i+1;j<L;j++) { int a=Arr1[i],b=Arr1[j]; if(a<b) { Arr1[i]=b; Arr1[j]=a; } } } } unsafe void TestFixed1() { fixed(int* A=Arr1) { int* arr1=A; for(int i=0;i<L;i++) arr1[i]=i; for(int i=0;i<L;i++) { for(int j=i+1;j<L;j++) { int a=arr1[i],b=arr1[j]; if(a<b) { arr1[i]=b; arr1[j]=a; } } } } } unsafe void TestFixed2() { fixed(int* A=Arr1) { for(int i=0;i<L;i++) A[i]=i; int* end=A+L; for(int *p=A;p<end;p++) { for(int* q=p+1;q<end;q++) { int a=*p,b=*q; if(a<b) { *p=b; *q=a; } } } } }
lair
21.09.2015 20:57+1Итак, скорость работы с массивами очень сильно зависит от того, где лежит переменная, представляющая этот массив.
Да. Компилятор пытается сделать предположения о том, кто может или не может изменять эту переменную параллельно с вами, и, как следствие, какие проверки надо встроить.
X_OSL
Если есть вопросы по измерениям, или его результатам, критика методики, анализа или другие комментарии — пишите. Буду рад прочитать, и ответить.
u_story
А какой jit использовался для C#: Старый или RyuJIT?
Возможно, что смена jit внесет свои корректировки?
X_OSL
Как писал ниже, для тестов использовал
Visual Studio 2010 SP1, С++ компилятор 16.00.40219.01, С# компилятор 4.0.30319.17929.
kekekeks
То биж говно мамонта для обоих языков.
X_OSL
Именно поэтому внизу привел результаты тестирования для VS2015:
«Не вдаваясь в детали: самая быстрая сортировка для C++ заняла 153105, а самая быстрая для C# 206552.
То есть разница порядка 30%»
ilnuribat
а можно хотя бы точки-запятые ставить в числах, а то 6-7 подряд идущих цифр тяжело воспринимаются, ещё тяжелее сравниваются
X_OSL
Вы имеете в виду какие идущие подряд числа?
Ununtrium
Не числа, а цифры. Числа вроде 148659643 нечитаемы, поэтому принято отделять запятыми/пробелами в районе тысяч.
X_OSL
Да, пожалуй 153 105 воспринимается легче чем 153105, но сразу появляются вопросы, не два ли это разных числа, или в случае 153,105 — отделение ли это дробной части.
Хотя возможно это мое субъективное восприятие проблемы, и запятая там где идут целые числа не должна вносить путаницу… Возможно я просто привык к отсутствию разделителей в цифрах…
DreamWalker
По результатам не хватает:
* Сравнение x86 и x64
* Сравнение LegacyJIT и RyuJIT
* Сравнение безопасного и небезопасного кода
* Сравнение результатов для разных размерностей массива (+ анализ того, каковы издержки на промохи кэша под каждую размерность на каждой железке)
* Сравнение разных версий рантайма (где Mono, где .NET Native и т.?п.)
По методике измерений не хватает:
* Грамотный прогрев бенчмарка (если вы измеряете холодный запуск, что весьма странно, то не мешало бы добавить сравнение с NGEN)
* Многократный запуск бенчмарка и анализ разброса значений (желательно делать запуски в разных процессах, т.?к. разные запуски CLR могут дать разные steady state)
* Есть ещё вагон и маленькая тележка разных тонких моментов, о которых нужно подумать, чтобы быть увереным в том, что бенчмарк даёт правдоподобные результаты.
По выводам:
* Содержательных выводов нет. Вы взяли какой-то очень специфичный пример программы, взяли очень специфичную конфигурацию для запуска, что-то померили, получили какие-то числа. Какой вывод должен сделать внимательный читатель? Что у управляемого кода есть некоторый overhead? Ну, это вроде бы и так было понятно.
* Для того, чтобы делать какие-то глобальные выводы в споре о производительности C++ vs C#, необходимо:
а) Смотреть не на один пример (который своим особым образом показывает издержки на обращение к элементу массива), а взять штук 600 разных примеров, каждый из которых проверяет отдельный аспект того или иного решения.
б) Смотреть на разные конфигурации. У языков самих по себе никакой производительности нет, измерять можно только скорость работы исполняемых файлов, которые получены в ходе компиляции. Стало быть, сравнивать надо компиляторы (для C# хотелось бы глянуть на выхлоп старого доброго csc и Roslyn-а под разные версии .NET Framework; Mono; .NET Native, который покажет вам совсем другие числа). Ну и запуск нужно проводить в разных окружениях, нынче их много.
в) Мне не нравится изначальная формулировка эксперимента «написать максимально идентичный и простой код на одном и другом языке». На мой взляд, при работе на современном железе зачастую проблемам скорости можно уделять не так много времени: многим не так важно, обработается ли, к примеру, пользовательский запрос за 50ms или 100ms. Если говорить о производительности, то нужно смотреть на такие ситуации, когда возникают проблемы с этой самой производительностью. И тут при сравнении языков разумней сравнивать не то, насколько различается производительность «максимально идентичного и простого кода», а то, насколько сложно эффективно решить ту или иную задачу на каждом из языков.
X_OSL
Спасибо за комментарий.
1. Сортировка х64 и 2015 студии показала 133875 — как лучший результат для С++ против 200469, как лучший результат для C#. Разница получилась даже больше чем для случая х86.
2. Я использовал стандартные 2015 студию и 2010 студию. Как можно включить использование LegacyJIT и RyuJIT в 2015 студии?
3. С небезопасным кодом возникает много вопросов, о том как именно его писать и насколько сделать безопасным. Если не сложно приведите пример самой простой сортировки переписанной на небезопасный код, наиболее правильным с вашей точки зрения методом.
4. Я проверял разные размерности массива. 10000 было выбрано по причине того что меньшие размерности давали слишком большую прогрешность и таким образом не давали выполнить сравнение, а большие лишь несколько увеличивали стабильность результата, но не меняли соотношение.
5. Mono действительно не сравнивал, так как очень мало использовал его, да и адекватный выбор платформы (ОС) на которой стоит проверять Mono это очень спорный вопрос. На Winodws врядли Mono целесоообразен, а альтернативных OC слишком много для того чтобы предоставить объективную картину.
6. Расскажите пожалуйста, как именно грамотный прогрев может повлиять на тесты, так же было бы интересно узнать как правильно прогревать .Net код.
7. Разброс на многократном запуске действительно был, порядка 3-5%, хочу также заметить что С++ на многократных запусках давал более стабильные результаты (отклонение менее 2%) в то время как С# давал до 5% отклонения между запусками.
8.Основной вывод в том что overhead есть и в грубой оценке его размера.
9. Конечно лучше смотреть больше примеров, в частности поэтому я сослался на одну из статей где примеров рассмотрено больше, есть и другие, однако как правило из результаты примерно в рамках 10..80% на overhead.
10. Сложно охватить все многообразие компиляторов и платформ, но к этому конечно надо стремиться в разумных рамках.
DreamWalker
1. Ну так эти результаты надо привести. JIT-x86 и JIT-x64 — это два разных JIT-компилятора. Вы приводите результаты для одного, а вывод делаете общий, это абсолютно некорректно. Например, LegacyJIT-x64 умеет разматывать циклы чётной длины: при снижении количество итераций с 10000 до 9999 время работы может увеличиться, т.?к. размотка цикла отключится. LegacyJIT-x86 такой размотки нет, там такого эффекта не будет.
2. Как я понимаю, RyuJIT у вас уже установлен (он идёт вместе с VisualStudio 2015 и .NET Framework 4.6), так что все x64-приложения под .NET 4.0+ запускаются из под RyuJIT. Рецепт отключения можно найти тут. Но на вашем месте я бы разобрался в теме намного подробнее: вы делаете выводы об эффективности JIT-компилятора, но при этом не знаете какого именно.
3. Сложно. Как написать сортировку «наиболее правильным методом» на неуправляемый код — это действительно не такая простая задача, тут думать надо. Как я уже отмечал выше, если стоит вопрос об эффективном решении задачи, то смысла писать простую неуправляемую сортировку нет — это академическая задачка, котоаря имеет мало отношения к реальной жизни. Если вам не подошла стандартная сортировка из BCL, то скорее всего у вашей задачи есть специфика, которую нужно учитывать при реализации этой самой сортировки.
4, 6, 7. Микробенчмаркинг — это очень сложная тематика. Я ей занимаюсь уже несколько лет, а у меня всё равно часто бенчмарки с ошибками получаются. Для грамотного замера времени C#-кода могу предложить попробовать BenchmarkDotNet. Для С++ конкретных советов дать не могу, но крайне советую поискать какие-нибудь библиотеки, которые позволят вам построить хороший бенчмарк и получить адекватные результаты.
5, 10. Ну тогда нужно предельно чётко обозначить границы вашего эксперимента во введении и заключении. Ещё раз: вы измеряете не эффективность C# а эффективность конкретных C#-компиляторов, JIT-компиляторов, конкретных реализаций .NET и т.?п. У меня есть много знакомых, которые каждый день пишут на C#, но при этом работают только с Mono. Чтобы не путать людей, нужно написать: «Результаты справедливы для Microsoft .NET Framework, версия такая-то, в качестве JIT используем RyuJIT, измеряем скорость доступа к элементу массива в управляемом коде и т. д.»
8, 9. Я не вижу особой пользы от выводов, которые приведены «в среднем по больнице». Польза будет от наблюдений вида «если в C# писать вот так, то работать будет столько, а если писать вот так, то вот столько». Непонятно, из чего складываются ваши 10..80%, какие штуки привносят в C# накладные расходы. В статье, на которую вы ссылаетесь, всё нормально написано, в выводах делается анализ: что на что влияет. В вашей статье анализа нет, а оценки накладных расходов не включают в себя правдоподобные доказательства.
X_OSL
1. х86 был выбран из-за большей совместимости. Тем более с х64 результаты по сути отличаются не сильно больше чем просто результаты на разных платформах. Но конечно х64 более актуально сейчас. Относительно размотки циклов, не уверен что она существенно поможет в рассмотренном примере.
2. Ага понятно, значит с 2015 студией я его и использовал. (C# компилятор 1.0.0.50618, C++ компилятор 19.00.23026)
3. В том то и дело, вопросов с написанием неуправляемого кода на С# кажется даже больше чем с кодом на С++. Сортировку я конечно писал не потому что мне не подошла стандартная, а исключительно для сравнения.
4,6,7. Попробую глянуть на библиотеку когда будет время. Однако думаю что все-таки «измеритель» для С++ и С# должен быть максимально идентичным, чтобы получать корректные результаты.
5,10. В статье я ссылался на то что результаты приведены исключительно для .Net Framework (при этом использовал разные его версии, и в конечном итоге 2 разных компилятора), Mono действительно оказалсе не охвачен, разве что немного в Head-to-head benchmark: C++ vs .NET.
8,9. Относительно различных реализаций тестовой задачи, как на одном, так и на другом языке думаю можно сделать выводы типа «если в C#(или С++) писать вот так, то работать будет столько, а если писать вот так, то вот столько». Конечно статья, на которую я ссылаюсь, более полная.
DreamWalker
1. Если постараться, то можно сочинить такой x64-пример, который под LegacyJIT-x64 и RyuJIT-x64 будет давать результаты, которые отличаются в два раза. Что уж говорить про разницу в платформах. Если вы делаете замеры для x86, то делайте выводы для x86. Если вы делаете выводы для всех платформ, то приведите замеры для всех платформ.
2. А в других комментариях вы писали про 4.0.30319.17929 для C#. В любом случае, C#-компилятор ? JIT-компилятор, это совершенно разные вещи, которые между собой не очень связаны.
3, 4, 6, 7. Вы пытаетесь сравнивать тёплое с мягким. Идентичным должен быть функционал кода, а не то, как он выглядит. Если вы используете на С++ решение, в котором нет проверок на выход за границы массива, то используйте в C# unsafe-решение без проверок. Если вы используете в C# код, который включает в себя проверку на выход за границы, то в С++ используйте решение с проверками. Вы же измеряете решение с проверками на одном языке и решение без проверок на другом. Какие-то результаты из вашего эксперимента получить можно, но я не уверен, что на их основе можно сформулировать содержательные выводы, которые не были очевидны до проведения эксперимента.
8, 9. У вас есть раздел «Выводы», но подобных фраз там нет.
X_OSL
1. Согласен, поэтому чтобы не быть голословным, в комментариях, добавил результаты для х64.
2. Это было в случае с VS2010, на которой были выполнены измерения в статье. А уже позже, в комментариях я провел измерения на VS2015. Результаты получились схожими (+\- 5%)
3,4,6,7. Я не уверен что C#, останется C#-ом в привычном нам понимании, если мы перепишем код на unsafe решение, таким образом полезность теста будет сомнительной. К тому же, как вы сами сказали в посте выше: «как написать сортировку «наиболее правильным методом» на неуправляемый код — это действительно не такая простая задача, тут думать надо.», что еще раз подтверждает то что это очень спорная задача для С# разработчика. В любом случае буду очень рад, если кто-нибудь напишет такой пример.
8,9. У меня есть некоторые выводы об этом в самой статье, да и многое видно по результатам измерений приведенных в таблицах. Заключительный же вывод я сделал более общим.
lair
Это очень спорная задача для любого разработчика, насколько я знаю. Если, конечно, речь идет о «написать сортировку», а не «написать что-то, похожее на сортировку, для имитации нагрузки».
X_OSL
Я имею в виду именно примитивную сортировку, такую же как приведена в статье, но с использованием unsafe кода C#.
И как я понял, DreamWalker видит именно задачу ее реализации в unsafe коде непростой.
DreamWalker
Не, unsafe-пузырёк я могу написать. Другой вопрос в том: зачем? Что именно вы хотите измерить? Без ответа на этот вопрос остальной разговор имеет мало смысла. Если вы хотите померить производительность операций чтения/записи над массивам, то сортировка вам не нужна. Если вы хотите померить то, насколько хорошо работает for, то сортировка вам тоже не нужна.
Если же вы хотите понять, насколько производительную сортировку можно написать на каждом из языков, то вам не подходят примитивные сортировки. Брать сортировку за N^2 и добиваться от неё хорошей производительности — странное академическое упражнение.
Сложность заключается как раз в выборе типа сортировки + особенностей её реализации под unsafe.
X_OSL
Напишите пожалуйста, а я прогоню его в тесте. Заодно сравним эффект от unsafe. Интересно и disassembly сравнить будет.
Конкретно в этом тесте измеряем скорость операций с элементами массива т.е. их чтение и запись. Сортировка лишь неплохой пример для вызова чтения\записи. Саму же сортировку конечно стоит проверять другими способами, но это уже к алгоритмам и за рамками данной статьи.
DreamWalker
Вы игнорируете мои основные тезисы. Нет смысла делать дополнительные прогоны, т.?к. в бенчмарке слишком много других проблем, я писал о них выше.
Сортировка — очень плохой пример для замеров эффективности чтения/записи. Вы прибавляете к этой операции кучу сайд-эффектов типа промахов кэша, итерации по массиву и т. п.
Если вы делаете академический эксперимент по измерению эффективности одной конкретной операции, то измеряйте одну конкретную операцию.
Если вы хотите решить реальную задачу, то решайте реальную задачу, а не делайте выводы о языке на основе n^2-сортировке на managed-коде. В вашем бенчмарке кроется огромное количество слабых мест, о которых вы ни строчке не написали.
X_OSL
Тем временем ниже, Mrrl, за что ему огромное спасибо уже написали пример, который используя unsafe код работает ни хуже С++ решения.
Ваши основные тезисы, как мне кажется сводятся к тому, что мерить производительность не нужно, потому-что сложно учесть все и сделать максимально адекватный тест. Я же хочу ее измерить.
Я не принимаю на веру утверждения о том что что-то работает быстро или медленно, пока сам это не измерю.
Считаете что бенчмарк плох — это ваше право. Вы можете написать лучше и агрументировано опровергнуть результаты.
EngineerSpock
Ага, да что там Акиньшин может в этом деле смыслить, правда же? (вы вообще заходили к Андрею в профиль и по ссылке библии, которую он вам скинул?)
Нет, я не сторонник аргументации личностями, но думаю здесь тот случай, когда вам стоит вдумчиво вчитаться во всё, что написал DreamWalker.
X_OSL
В своих суждениях я исхожу лишь из прочитаных постов, и разумеется мне было бы интересно почитать статьи DreamWalker-а с анализом производительности С# и C++, если бы таковые были.
Я не говорю о том что DreamWalker пишет неправильные вещи, я во многом согласен с тем что он пишет, однако, как мне кажется, он пытается так или иначе уйти от прямого сравнения производительности, и вот уже это мне не понятно.
DreamWalker
Сравнение C# и C++ по производительности — очень странная тема.
1. Я считаю, что некорректно сравнивать производительность языков. У языков нет производительности. Вы можете сравнивать только код, который выдаёт конкретный компилятор C++, с кодом, который выдаёт конкретный компилятор C# (который запускается по конкретной версией .NET-рантайма с конкретным JIT-компилятором). Нельзя взять какую-то одну конфигурацию для запуска, на основе которой делать выводы про язык.
2. В своём посте вы пытаетесь сравнивать C++ программу и C# программу, которые похожи внешне, не задумываясь о том, что
items[i]
в C++ и C# означают разные вещи.3. Я считаю, что если уж сравнивать разные языки программирования по скорости, то нужно брать задачу и пытаться решить её максимально эффективно на каждом из языков, после чего проанализировать: насколько сложным получились наиболее эффективные решения, насколько быстро они работают.
4. Если хорошо понимать методику работы конкретного JIT + понимать как работает CPU, то на C# вполне можно добиться высокой эффективности для конкретного небольшого метода, получится не хуже, чем в C++. В споре C++ vs C# самое интересное для обсуждения — накладные расходы от самого рантайма (например, GC), эффективность работы стандартный классов, которыми все пользуются, и т.?п.
Это прекрасно, что вы заинтересовались темой производительности. Я считаю, что подобные эксперименты помогут вам лучше разобраться с тем, что находится под капотом ваших программ. Но если вы решаете опубликовать свои наработки (например, на Хабре), постарайтесь удостовериться, что материал несёт в себе полезную информацию, которая основывается на хороших экспериментах и может принести пользу другим программистам.
X_OSL
1. Согласен, но что мешает сравнивать результат компиляции? Ведь без компиляции язык не может быть выполнен, а нас интересует именно время выполнения.
2. С точки зрения решения прикладной задачи items[i] и в C++ и в C# значат одно и тоже — доступ к элементу массива. В чем вы видите разницу?
3. Я пытался. Но моя задача была не максимально быстро отсортировать массив, а максимально быстро прочитать\записать элемент массива. (в исходниках кстати есть и пример максимально быстрой сортировки, в которой С++ тоже был быстрее, но тут мы не меряем скорость сортировки)
4. Тут уже нужны конкретные примеры. Пока в статье есть только один пример с unsafe кодом, который показал схожую с С++ производительность доступа к элементам массива.
Я искренне надеюсь что данная статья способна принести пользу многим программистам.
DreamWalker
1. Компиляторов много, .NET рантаймов много. Есть же, к примеру, .NET Native: можно писать программу на том же C#, а при компиляции использовать back end от компилятора C++. А ещё я могу написать собственный компилятор С++, который будет выдавать очень медленный код: но это же не будет означать, что С++ плохой, это будет означать, что мой компилятор плохой.
2. ECMA-334, Раздел 14.5.6.1 «Array access» гласит, что при обращении к элементу массива по индексу должна быть совершена проверка на выход за границе массива. Да, конструкции выглядят одинаково, означают похожие вещи, но разница в том, что
items[i]
в C# делает проверку, а в C++ нет.3. Ну так зачем тогда писать сортировку и добавлять сайд-эффекты? Хотите измерить чтение из массива — измеряйте чтение из массива (хотя это крайне странная задача для сравнения C++ vs C#).
X_OSL
1. Я был бы очень рад увидеть результаты тестирования с .Net Native относительно С++. Хотелось бы оперировать результатами чтобы делать выводы относительно эффктивности.
2. Получается мой тест оценивает цену этой проверки с точки зрения производительности, разве это не полезно? И разумеется когда я выполняю доступ к элементу, я не всегда хочу выполнять проверку. Поэтому да — проверка в ряде случаев чистый overhead, не нужный для решения задачи.
Кстати, очень интересно, останется ли проверка в .Net Native?
3. Я хотел посмотреть на работу «не последовательного» доступа к элементам. Поэтому решил использовать алгоритм сортировки… Конечно есть и другие варианты такого доступа, но этот вариант мне показался одним из наиболее простых для восприятия и понятных.
DreamWalker
Ваш тест косвенно показывает, что если перед взятием значения элемента из массива выполнить проверку на индекс, то время работы программы увеличится. Но этот факт видится мне достаточно очевидным, так что я всё ещё не могу понять пользу ваших выводов.
Вы уже начинаете задавать правильные вопросы.
Давайте разделять задачи. Если мы измеряем доступ к элементу, то давайте измерять доступ к элементу. Если вы хотите посмотреть, насколько быстро можно последовательно обратиться к каждому из элементов массива, то посчитайте сумму элементов. Если вас интересует рандомизированный доступ к массиву, то тут скорее надо вести речь о промохах кэша, о размерах L1/L2/L3, о работе CPU. Постарайтесь поставить себе задачу максимально чётко и решайте именно её.
Я постараюсь подвести итог нашей дискуссии. В комментариях к этому посту много людей старалось вам объяснить, что ваш бенчмарк не является корректным, а результаты не несут особой значимости и полезности. Вам дали много хороших советов о том, как следует подходить к рассмотрению подобных тем. Настоятельно рекомендую внимательно всё просмотреть, изучить соответствующий материал и использовать новые знания для своих следующих работ.
X_OSL
Я очень четко поставил себе задачу — оценить накладные расходы на «управление кодом» путем сравнения решений на С++ и С#. И как мне кажется оценку удалось получить вполне показательную.
Я буду очень рад, если вы приведете ее опровержение или же напишите сравнение по методикам которые считаете верными, мы их сможем обсудить и сделать выводы.
DreamWalker
Я устал с вами спорить, это мой последний комментарий.
Рассказываю последний раз: вы написали некоторую программу на C++, затем написали похожую программу на C#, попробовали запустить обе программы в весьма ограниченном списке окружений, получили какие-то числа, после чего делаете вывод про языки в целом.
Общий вывод о том, что .NET-программа в среднем работает дольше нативной за счёт расходов на управляемую среду, был понятен и до вашего исследования. Никто не говорит, что результат не является верным, все говорят, что ваш эксперимент содержит много ошибок и не может рассматриваться как доказательство этого результата в общем случае.
Детально все свои замечания я уже высказал выше. Почитайте также хорошие советы от других людей: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33.
dprotopopov
Спасибо.
Вы чётко дали ответ на поставленную задачу.
Результат интересен.
Теоретизировать можно много создавая мысленные абстракции, но практика — критерий истины.
Mrrl
В данном случае обе программы (на C++ и C#) осуществляют доступ к элементам одного и того же массива, одинаково расположенного в памяти (последовательные ячейки, а не беспорядочная куча байтов), элементы просматриваются и изменяются в одном и том же порядке. Поэтому все особенности работы данной памяти на данном процессоре в C# и C++ проявляются одинаково (для сортируемых элементов), и на них можно «сократить».
Что останется? Число исполняемых команд, время на выполнение, размещение в памяти промежуточных результатов, повторные обращения к элементам массива… В общем, все «накладные расходы», на уменьшение которых направлены оптимизации компиляторов. И понять, какой из них справляется лучше, и насколько С# мешает необходимость проверять индексы — вполне нормальная задача. Память тут ни при чём. Наша задача — обеспечить одинаковый порядок работы с ней, чтобы не дать преимущества одному из компиляторов. А дальше пусть они разбираются сами.