Как стать автором
Обновить

Комментарии 212

Если есть вопросы по измерениям, или его результатам, критика методики, анализа или другие комментарии — пишите. Буду рад прочитать, и ответить.
А какой jit использовался для C#: Старый или RyuJIT?
Возможно, что смена jit внесет свои корректировки?
Как писал ниже, для тестов использовал
Visual Studio 2010 SP1, С++ компилятор 16.00.40219.01, С# компилятор 4.0.30319.17929.
Именно поэтому внизу привел результаты тестирования для VS2015:
«Не вдаваясь в детали: самая быстрая сортировка для C++ заняла 153105, а самая быстрая для C# 206552.
То есть разница порядка 30%»
а можно хотя бы точки-запятые ставить в числах, а то 6-7 подряд идущих цифр тяжело воспринимаются, ещё тяжелее сравниваются
Вы имеете в виду какие идущие подряд числа?
Не числа, а цифры. Числа вроде 148659643 нечитаемы, поэтому принято отделять запятыми/пробелами в районе тысяч.
Да, пожалуй 153 105 воспринимается легче чем 153105, но сразу появляются вопросы, не два ли это разных числа, или в случае 153,105 — отделение ли это дробной части.

Хотя возможно это мое субъективное восприятие проблемы, и запятая там где идут целые числа не должна вносить путаницу… Возможно я просто привык к отсутствию разделителей в цифрах…
По результатам не хватает:
* Сравнение 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. Если говорить о производительности, то нужно смотреть на такие ситуации, когда возникают проблемы с этой самой производительностью. И тут при сравнении языков разумней сравнивать не то, насколько различается производительность «максимально идентичного и простого кода», а то, насколько сложно эффективно решить ту или иную задачу на каждом из языков.
Спасибо за комментарий.

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. Сложно охватить все многообразие компиляторов и платформ, но к этому конечно надо стремиться в разумных рамках.
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# накладные расходы. В статье, на которую вы ссылаетесь, всё нормально написано, в выводах делается анализ: что на что влияет. В вашей статье анализа нет, а оценки накладных расходов не включают в себя правдоподобные доказательства.
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#(или С++) писать вот так, то работать будет столько, а если писать вот так, то вот столько». Конечно статья, на которую я ссылаюсь, более полная.
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. У вас есть раздел «Выводы», но подобных фраз там нет.
1. Согласен, поэтому чтобы не быть голословным, в комментариях, добавил результаты для х64.
2. Это было в случае с VS2010, на которой были выполнены измерения в статье. А уже позже, в комментариях я провел измерения на VS2015. Результаты получились схожими (+\- 5%)
3,4,6,7. Я не уверен что C#, останется C#-ом в привычном нам понимании, если мы перепишем код на unsafe решение, таким образом полезность теста будет сомнительной. К тому же, как вы сами сказали в посте выше: «как написать сортировку «наиболее правильным методом» на неуправляемый код — это действительно не такая простая задача, тут думать надо.», что еще раз подтверждает то что это очень спорная задача для С# разработчика. В любом случае буду очень рад, если кто-нибудь напишет такой пример.
8,9. У меня есть некоторые выводы об этом в самой статье, да и многое видно по результатам измерений приведенных в таблицах. Заключительный же вывод я сделал более общим.
К тому же, как вы сами сказали в посте выше: «как написать сортировку «наиболее правильным методом» на неуправляемый код — это действительно не такая простая задача, тут думать надо.», что еще раз подтверждает то что это очень спорная задача для С# разработчика.

Это очень спорная задача для любого разработчика, насколько я знаю. Если, конечно, речь идет о «написать сортировку», а не «написать что-то, похожее на сортировку, для имитации нагрузки».
Я имею в виду именно примитивную сортировку, такую же как приведена в статье, но с использованием unsafe кода C#.
И как я понял, DreamWalker видит именно задачу ее реализации в unsafe коде непростой.
Не, unsafe-пузырёк я могу написать. Другой вопрос в том: зачем? Что именно вы хотите измерить? Без ответа на этот вопрос остальной разговор имеет мало смысла. Если вы хотите померить производительность операций чтения/записи над массивам, то сортировка вам не нужна. Если вы хотите померить то, насколько хорошо работает for, то сортировка вам тоже не нужна.
Если же вы хотите понять, насколько производительную сортировку можно написать на каждом из языков, то вам не подходят примитивные сортировки. Брать сортировку за N^2 и добиваться от неё хорошей производительности — странное академическое упражнение.
Сложность заключается как раз в выборе типа сортировки + особенностей её реализации под unsafe.
Напишите пожалуйста, а я прогоню его в тесте. Заодно сравним эффект от unsafe. Интересно и disassembly сравнить будет.

Конкретно в этом тесте измеряем скорость операций с элементами массива т.е. их чтение и запись. Сортировка лишь неплохой пример для вызова чтения\записи. Саму же сортировку конечно стоит проверять другими способами, но это уже к алгоритмам и за рамками данной статьи.
Вы игнорируете мои основные тезисы. Нет смысла делать дополнительные прогоны, т. к. в бенчмарке слишком много других проблем, я писал о них выше.
Сортировка — очень плохой пример для замеров эффективности чтения/записи. Вы прибавляете к этой операции кучу сайд-эффектов типа промахов кэша, итерации по массиву и т. п.
Если вы делаете академический эксперимент по измерению эффективности одной конкретной операции, то измеряйте одну конкретную операцию.
Если вы хотите решить реальную задачу, то решайте реальную задачу, а не делайте выводы о языке на основе n^2-сортировке на managed-коде. В вашем бенчмарке кроется огромное количество слабых мест, о которых вы ни строчке не написали.
Тем временем ниже, Mrrl, за что ему огромное спасибо уже написали пример, который используя unsafe код работает ни хуже С++ решения.

Ваши основные тезисы, как мне кажется сводятся к тому, что мерить производительность не нужно, потому-что сложно учесть все и сделать максимально адекватный тест. Я же хочу ее измерить.

Я не принимаю на веру утверждения о том что что-то работает быстро или медленно, пока сам это не измерю.

Считаете что бенчмарк плох — это ваше право. Вы можете написать лучше и агрументировано опровергнуть результаты.
Ваши основные тезисы, как мне кажется сводятся к тому, что мерить производительность не нужно, потому-что сложно учесть все и сделать максимально адекватный тест. Я же хочу ее измерить.

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

Я не говорю о том что 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), эффективность работы стандартный классов, которыми все пользуются, и т. п.

Это прекрасно, что вы заинтересовались темой производительности. Я считаю, что подобные эксперименты помогут вам лучше разобраться с тем, что находится под капотом ваших программ. Но если вы решаете опубликовать свои наработки (например, на Хабре), постарайтесь удостовериться, что материал несёт в себе полезную информацию, которая основывается на хороших экспериментах и может принести пользу другим программистам.
1. Согласен, но что мешает сравнивать результат компиляции? Ведь без компиляции язык не может быть выполнен, а нас интересует именно время выполнения.
2. С точки зрения решения прикладной задачи items[i] и в C++ и в C# значат одно и тоже — доступ к элементу массива. В чем вы видите разницу?
3. Я пытался. Но моя задача была не максимально быстро отсортировать массив, а максимально быстро прочитать\записать элемент массива. (в исходниках кстати есть и пример максимально быстрой сортировки, в которой С++ тоже был быстрее, но тут мы не меряем скорость сортировки)
4. Тут уже нужны конкретные примеры. Пока в статье есть только один пример с unsafe кодом, который показал схожую с С++ производительность доступа к элементам массива.

Я искренне надеюсь что данная статья способна принести пользу многим программистам.
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#).
1. Я был бы очень рад увидеть результаты тестирования с .Net Native относительно С++. Хотелось бы оперировать результатами чтобы делать выводы относительно эффктивности.

2. Получается мой тест оценивает цену этой проверки с точки зрения производительности, разве это не полезно? И разумеется когда я выполняю доступ к элементу, я не всегда хочу выполнять проверку. Поэтому да — проверка в ряде случаев чистый overhead, не нужный для решения задачи.
Кстати, очень интересно, останется ли проверка в .Net Native?

3. Я хотел посмотреть на работу «не последовательного» доступа к элементам. Поэтому решил использовать алгоритм сортировки… Конечно есть и другие варианты такого доступа, но этот вариант мне показался одним из наиболее простых для восприятия и понятных.
Получается мой тест оценивает цену этой проверки с точки зрения производительности, разве это не полезно?

Ваш тест косвенно показывает, что если перед взятием значения элемента из массива выполнить проверку на индекс, то время работы программы увеличится. Но этот факт видится мне достаточно очевидным, так что я всё ещё не могу понять пользу ваших выводов.

Кстати, очень интересно, останется ли проверка в .Net Native?

Вы уже начинаете задавать правильные вопросы.

Я хотел посмотреть на работу «не последовательного» доступа к элементам.

Давайте разделять задачи. Если мы измеряем доступ к элементу, то давайте измерять доступ к элементу. Если вы хотите посмотреть, насколько быстро можно последовательно обратиться к каждому из элементов массива, то посчитайте сумму элементов. Если вас интересует рандомизированный доступ к массиву, то тут скорее надо вести речь о промохах кэша, о размерах L1/L2/L3, о работе CPU. Постарайтесь поставить себе задачу максимально чётко и решайте именно её.

Я постараюсь подвести итог нашей дискуссии. В комментариях к этому посту много людей старалось вам объяснить, что ваш бенчмарк не является корректным, а результаты не несут особой значимости и полезности. Вам дали много хороших советов о том, как следует подходить к рассмотрению подобных тем. Настоятельно рекомендую внимательно всё просмотреть, изучить соответствующий материал и использовать новые знания для своих следующих работ.
Я очень четко поставил себе задачу — оценить накладные расходы на «управление кодом» путем сравнения решений на С++ и С#. И как мне кажется оценку удалось получить вполне показательную.

Я буду очень рад, если вы приведете ее опровержение или же напишите сравнение по методикам которые считаете верными, мы их сможем обсудить и сделать выводы.
Я устал с вами спорить, это мой последний комментарий.

Рассказываю последний раз: вы написали некоторую программу на 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.
Спасибо.
Вы чётко дали ответ на поставленную задачу.
Результат интересен.
Теоретизировать можно много создавая мысленные абстракции, но практика — критерий истины.
Давайте разделять задачи. Если мы измеряем доступ к элементу, то давайте измерять доступ к элементу. Если вы хотите посмотреть, насколько быстро можно последовательно обратиться к каждому из элементов массива, то посчитайте сумму элементов. Если вас интересует рандомизированный доступ к массиву, то тут скорее надо вести речь о промохах кэша, о размерах L1/L2/L3, о работе CPU. Постарайтесь поставить себе задачу максимально чётко и решайте именно её.

В данном случае обе программы (на C++ и C#) осуществляют доступ к элементам одного и того же массива, одинаково расположенного в памяти (последовательные ячейки, а не беспорядочная куча байтов), элементы просматриваются и изменяются в одном и том же порядке. Поэтому все особенности работы данной памяти на данном процессоре в C# и C++ проявляются одинаково (для сортируемых элементов), и на них можно «сократить».
Что останется? Число исполняемых команд, время на выполнение, размещение в памяти промежуточных результатов, повторные обращения к элементам массива… В общем, все «накладные расходы», на уменьшение которых направлены оптимизации компиляторов. И понять, какой из них справляется лучше, и насколько С# мешает необходимость проверять индексы — вполне нормальная задача. Память тут ни при чём. Наша задача — обеспечить одинаковый порядок работы с ней, чтобы не дать преимущества одному из компиляторов. А дальше пусть они разбираются сами.
Верно, в C# при каждом обращении по индексу происходит проверка за пределы массива. Но этого можно избежать с использованием небезопасного кода на C# — в таком случае результат должен быть идентичен производительности кода на C++, где также нет проверок вхождения значения в диапазон при каждом обращении к массиву.

Это конечно не значит, что всюду стоит использовать небезопасный код. Но для каких-то критичных участков кода это может быть полезным.
В тесте, я попробовал просто обнести сортировку блоком 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],ecx
Все-таки, при правильном получении 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
Не для raw массивов, но для array и vector: можно поиграться с at() и operator[] — оба делают одно дело, но: первый проверяет границу и бросает исключение, второй — нет, что бы полностью соответствовать семантике своего raw-собрата и не давать просадки в производительности, где они не ожидаются.
Переход на fixed увеличил время работы примерно в 1.6 раза — с 5.7 сек до 9.6 (на 100 сортировок). VS 2013, .NET 4, Any CPU
Код в 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 sec
Интересно, — спасибо. Unsafe оптимизация не очевидная конечно. Попробую добавить в тест такой вариант.
У меня под 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;
}
}
}

Что я делаю не так?..
Интересно, почему он каждый раз достаёт I.

(предположение) потому что раз уж вы полезли в unsafe, вы лучше знаете, чего вы хотите, и не надо вам мешать?
Я тоже так думаю. Но как добиться от него помощи — чтобы и индексы не проверял, и указатель доставал один раз, и не нужно было подключать unmanaged код, а можно было оставаться в C#?
По-моему, для «отключения» проверки индексов достаточно сравнивать с array.Length. Ниже это обсуждается.
Да, 4.3 сек (вместо 7.5). Вполне нормально — там, где это работает (где индексы просто перебираются, а не достаются из других таблиц).
Если внимательно посмотреть тот код, то можно увидеть, что одна проверка индекса там осталась:
01322E1B mov eax,dword ptr [ebp-14h] 
 01322E1E cmp ebx,eax 
 01322E20 jae 01322E46 

Возможно, проверки отключаются только для цикла от 0 до array.Length, а если начинать с другого значения, то остаются.
Итак:
            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 указатели надо дублировать :)
Переписал через указатели:
                    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] может стоить очень много.
Даже для С++ разработчика данный код выглядит жестко.

Но у меня в тесте он показал производительность примерно(а рамках погрешности измерений) равную производительности С++ кода не переписанного подобным образом.

В целом это выход, если очень нужна производительность, но конечно очень не простой для среднестатистического С# разработчика.

Даже для С++ разработчика данный код выглядит жестко.

Зато для C-разработчика это набор идиом :)
Согласен, это практический чистый С.
Если бы это было так просто, например:

«Существуют особые ситуации, когда 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.
Давайте напишем 10 строчек абстрактного кода и сделаем далеко идущие выводы; обоснуем выводы таблицами и графиками.

Подбробнее
Какое-то однобокое сравнение. Постараюсь объяснить свою точку зрения.
Во-первых, вы из замеров 10 строчек пытаетесь делать выводы.
Во-вторых, мы знаем что C++ выбирают для числодробилок, а C# это энтерпрайз. Возможно стоит взять несколько пар средних по размеру текста алгоритмов. Например, физические вычисления и парсинг большого xml. А потом сделать выводы:
Вывод 1: если полениться и реализовать числодробилку на С#, то получится такой-то штраф по скорости
Вывод 2: если напрячься и реализовать энтерпрайз на C++, то мы получим такие-то ускорения
Я хотел разобрать максимально простой пример, который при этом будет легко читаться в ассемблере. Мне кажется сложное складывается из простого.

Проанализировать более сложный алгоритм в ассемблере разумеется тоже реально, но его реализация на каждом из языков будет вызывать много спорных моментов (почему использована именно эта библиотека, почему использован именно этот тип, этот вид оптимизации и т.п.)
Ведь даже в моем примитивном примере пришлось рассмотреть альтернативные контейнеры и некоторые элементы оптимизации. В более сложном алгоритме, количество подобных вариаций думаю должно быть существенно больше, чтобы выполнить более менее объективное сравнения, это требует многократно больше времени.

Мне кажется анализировать элементы сложного проще, чем сложное целиком.

И кстати, попытка анализа чуть более сложных алгоритмов есть в статье Head-to-head benchmark: C++ vs .NET, на которую я ссылаюсь в своей статье.
Если разбираете максимально простой пример, то вывод можно сделать только один — «в максимально простых примерах одно лучше другого на 10%-80%». Всё.

Еще раз: на основании ваших измерений нельзя делать больших выводов. В противном случае получится как в картинке «my hobby extrapolating»…
Но я думаю, что вы согласитесь, что даже самые сложные решения состоят из максимально простых элементов.

В разобранном примере продемонстрированы издержки на managed среду (в частности, дополнительные проверки), и думаю даже в самом сложном примере задача обеспечения управления кодом никуда не уйдет.

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

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

Но ведь я говорю чистую правду. Сложное состоит из простого, по крайней мере в разработке ПО.
Сложное состоит из простого, по крайней мере в разработке ПО.

Это — правда. А ваша экстраполяция это манипулирование с целью оправдать вашу известную предвзятость на счет темы managed и unmanaged языков.
Моя экстраполяция это не манипулирование.

Считаете ли вы что менеджмент кода возможно осуществлять без накладных расходов? Если нет, то в чем я не прав, экстраполируя факт наличия расходов на менеджмент простого кода на более сложные случаи?
Если нет, то в чем я не прав, экстраполируя факт наличия расходов на менеджмент простого кода на более сложные случаи?

Очевидно, в соотношении этих расходов к общему времени выполнения кода; иначе говоря — в количественной оценке потери производительности.
Я даже специально раздел в статье написал «Какая же все-таки реальная оценка?», где указал на это соотношение в реальных задачах.
И там не написано ни одной конкретной цифры.

Возьмем, скажем, типовую такую энтерпрайз-задачу — у меня есть веб-сервис, принимающий SOAP-сообщения мегабайт в десять-пятнадцать на сообщение; дальше эти сообщения разбираются, протоколируются в БД, трансформируются и маршрутизируются дальше на следующий сервис.

Какова будет разница в производительности этого сервиса на среднестатистическом серверном оборудовании при его реализации на C++ и C#?
Конкретная цифра будет разной в каждой конкретной задаче. Вы должны знать сколько процентов runtime приходится на ваш сервис, например померив этот процент профайлером и далее делать выводы о необходимости оптимизации.
Ага, и откуда мы будем знать, сколько процентов приходится на сервис до его написания? Получается, что ваши тесты не дают никакой дополнительной информации для принятия решения, на чем писать ту или иную систему.

Я поэтому и говорю: нет ни одного способа формально масштабировать результаты ваших тестов на реальные задачи.
Тесты измеряют лишь скорость выполнения «вашего» кода.

Сколько же времени придется на «ваш» код, а сколько на код системы вы должны определить профайлером или же вывести из собственного опыта в реализации или профилировании подобных задач.

Масштабировать результаты возможно, используя соотношение между временем выполнения вашего кода и кода системы. Соотношение нужно знать априорно, из опыта разработки.
Предположим, из общего времени выполнения соотношение между «моим» кодом и «системным» — 50/50. Отдельно заметим, что загрузка CPU выше 40% не поднимается все время работы.

И как результаты ваших тестов масштабируются на время работы моего сервиса?
Тогда можно предположить, что использование managed кода даст дополнительные потери производительности в диапазоне 5-40%, и если загрузка CPU лишь 40%, то порядка 40% от этого диапазона, то есть ориентировочные потери будут лишь 2-16%
(если правда 40% загрузка)
А откуда вы берете цифры «дополнительных потерь» 5-40%?
Промасштабировал 10-80%, полученные в моих, и не только моих тестах.
Но почему вы считаете, что разница в производительности c# и c++ на этой задаче будет составлять те же 10-80%?

(Не говоря уже о том, что само по себе измерение, дающее восьмикратный разброс показаний — это печаль)
Различные тесты(не только мои) подтверждают примерно такую разницу.

По скольку тестов опровергающих данную разницу нет, считаю оправданным ее использование.

И разумеется восьмикратный разброс вполне объясним т.к. зависит от задачи.
В этих тестах использовался тот же код (хотя бы родственный), что в моей задаче? Хотя бы примитивы использовались те же?
Думаю общие принципы генерации managed кода в тестах схожи с вашим случаем. Думаю что принципы применяются схожим образом для разных типов. Хотя конечно в каждом случае могут быть свои нюансы.
Но управление кодом вряд-ли может быть бесплатным.
«Общие принципы» — это бессмысленные слова, они не дают никаких конкретных цифр, как следствие — не позволяют оценить конкретный выигрыш/проигрыш.

Управление кодом, конечно, не может быть бесплатным, но вопрос конкретных затрат на него.
Я предложил вариант их оценки (вернее и задолго до меня такие варианты были предложены)

Какой вариант предложили бы вы?
Есть ровно один вариант оценки производительности применительно к конкретной задаче — реализация этой задачи и сравнение производительности реализаций. После накопления достаточного количества «типовых» задач можно будт делать какие-то количественные оценки.

(Заметим, качественная оценка — «скорее всего, оптимизированный c++ будет быстрее оптимизированного c#» — всем интуитивно понятна, только толку с нее немного в практическом применении)
Троллинг чистой воды.
1. До разработки двух версий узнать оценку разницы производительности нельзя, имея на руках любые синтетические тесты.
2. Наличие синтетических тестов позволяет сделать хоть какую то оценку, а вопрос ее достаточности и достоверности каждый принимает сам.

Соответственно, Вы задаете вопрос: с чего 80%?
Вам отвечают: ну вот у меня тесты, я ОЦЕНИВАЮ что будет ПРИМЕРНО так же.
Вы: а что мне эти тесты, они вообще не про мою систему
Вот я и пытаюсь понять степень достоверности оценки производительности веб-сервиса, сделанной на основании замеров скорости сортировки в памяти. Пока что выходит, что она где-то на уровне случайной величины.
Картинка была такая у xkcd:
мое хобби - экстраполяция
image

«Я сделал сортировку на C++ на 80% быстрее чем C#! Ура, C++ в 5 раз быстрее!!!» — только вот если уйти из мира волшебных единорогов в которых вся программа — это сортировка то выясняется что сортируется файл с диска, а перед этим его скачивают из сети и вообще сортировка занимает 3 секунды и выполняется в фоновом режиме, а в общем по коду «тяжелые» участки которые вы «ускорили» занимают 10%, а остальные 90% «ускоряются» на уровень погрешности. А то напоминает историю как люди ускорили в 2 раза код который выполнялся 80% времени, правда выполнялся он все равно 80% времени, потому что толку оптимизировать idle loop небыло.
Я же все это описал. Нужно просто знать сколько времeни занимает ваш код в runtime.
И это не всегда показатель. Если это какое-нибудь пользовательское приложение то пользователю без разницы занимаете оно 5% CPU или 25%, а уже тем более если это проценты одного ядра. Если вы пишете чат и время парсинга ответа сократили с 5мс до 1мс на референсной системе — толку от этого ноль, пользователь на глаз не отличит. Ну и кроме того даже если у вас процессор загружен по максимуму и оптимизаци позволят сэкономить на количестве серверов (или эквиваленте) — возможно дешевле будет добавить оперативки или докупить этих серверов. Задач где действительно так важна разница в производительности замеренных порядков (т.е. это же не 10 и не 100 раз) — мало, да и в них эти синтетические «80%» будут скорее всего далеки от истины. Кроме того сравнили вы два конкретных компилятора. Сегодня разница может быть X, а завтра выйдет новый компилятор и разница уже Y. А для C++ еще есть компиялатор от Intel — можно сразу с ним сравнивать.
Теперь вопрос, а откуда вы знаете, что эти 40% времени делает процессор? Алгоритм может быть таков, что постоянно генерирует cache miss и в managed, и в unmanaged реализации. Большую часть времени процессор простаивает, ожидая данных, и, допустим, грузит процессор на 100%. Разве не можем мы здесь получить меньше 5% потерь? Ваша методика как-то учитывает это?
Было бы интересно, если бы вы привели пример такого теста, мы бы сравнили результаты выполнения managed и unmanaged реализации и сделали бы выводы.
Т.е. ваша методика не учитывает банальнейшей ситуации неэффективной работы с памятью, отчего ее нельзя экстраполировать даже на другую небольшую задачку перемалывания чисел, в чем так хорош C++, не говоря уже про enterprise решения. Вам нужно модифицировать вашу оценку с 5-80% на 0% до +∞, тогда она будет более верной, но, к сожалению, все такой же бесполезной.
Опять таки, приведите примеры тестов, которые бы учитывали неэффективную работы с памятью, думаю всем будет интересно почитать статью про это.
Ради любопытства я все таки тест сделал. Код — банальнейший пример cache miss, который на каждой лекции по теме показывают. Заполнение двумерного массива 10000x100000 вложенными циклами. Перестановка циклов — cache miss пропадают и время выполнения уменьшается на порядки. Я сделал даже сложнее вариант — заполнение элементами из противоположного конца массива, чтоб побольше промахов было. Невероятным образом получил на 2012 студии и .Net 4.5.1 результат такой, что C# стабильно быстрее на 40-50 миллисекунд в масштабах 30 секунд общего времени. Статью я из этого по понятным причинам делать не буду, результат забавный, не более.
Пожалуйста, выложите хотя бы исходники этого сравнения, очень интересно посмотреть как именно реализованы C++ и С# варианты.
Ответили же:
Заполнение двумерного массива 10000x100000 вложенными циклами.
Или вы вообще не в курсе что такое cache miss? Ну и зачем тогда писать статью о «производительности» если даже базовые вещи надо разжевывать?
Было бы неплохо увидеть точную реализацию (хотя общая идея и понятна)
Если вы не знаете как работает подсистема памяти и кэш в частности — так и скажите. Что может быть проще двух вложенных циклов заполняющих двумерный массив? Ну зайдите в гугл чтоли посмотрите.
Вопрос итератор какого из циклов за какой индекс массива будет отвечать и наличия другого кода.

cache miss, по идее должен возникать когда будет обращение к элементам массива находящимся друг от друга на расстоянии больше, чем размер кэша. То есть вложенным должен поидее быть итератор, который отвечает за итерацию по «колонкам»(с точки зрения расположения в памяти) двухмерного массива.

И другой вопрос в том как именно данный пример сравнивает С++ и C#?
Кто-то из них способен избежать эффекта cache miss?
Вопрос итератор какого из циклов за какой индекс массива будет отвечать и наличия другого кода.
А что, есть варианты?
cache miss, по идее должен возникать когда будет обращение к элементам массива находящимся друг от друга на расстоянии больше, чем размер кэша.
Что и подтверждает мои слова о том что вы не в курсе как работает кэш. Почитайте хотябы вики на досуге, кэш работает по линиям. Нет, ну конечно если «больше чем размер кэша» — тоже miss будет, но условие совершенно не обязательное.
И другой вопрос в том как именно данный пример сравнивает С++ и C#?
Кто-то из них способен избежать эффекта cache miss?
А вы выше читайте что вам сказали, а то уже забыли на что отвечали.
Я несколько раз прочитал все написанное выше, и не увидел ответа на вопрос:

Почему эффект cache miss будет разным для С++ и С# при работе с данными одинакового вида и использовании одинакового алгоритма обработки?
Он не будет разным, читайте внимательнее к чему вам это сказали.
До тех пор, пока вы не конкретизируете эти сложные случаи, ваши слова останутся манипулированием. А после станут голословными утверждениями, потому что оценка расходов требуется в каждом конкретном случае. В этом вы не правы. Ваша статья не имеет абсолютно никакого смысла, кроме как — пузырьковая сортировка работает на С++ быстрее при конкретных условиях. Больше никуда эту статью экстраполировать невозможно.
А какую оценку оценку производительности или методику, которую можно экстраполировать на сложные случаи по вашему мнению?
По началу статьи догадывался, что написал ее автор и этой статьи: Выбор между C++ и C#. Открыл профиль, и мои догадки подтвердились. Почитайте комментарии к ней.
В этой статье дана прямая ссылка на упомянутую вами статью. И разумеется я читал все комментарии к ней и на большую часть из них отвечал.

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

Есть ещё один нюанс: если посадить нуба писать на плюсах, то он напишет глючное тормозное поделие. Если этого же нуба посадить писать на шарпе, то с большой вероятностью код будет работать быстрее. Это происходит по многим причинам: нуб будет воевать с плюсами, а не оптимизировать; «классы по умолчанию» в дотнете подобраны лучше и так далее. То же верно не только для нубов, но и для хороших специалистов, которых менеджеры подгоняют писать быстрее.

Так как в энтерпрайзе количество гениев ограничено, а крайний срок всегда «вчера», то внезапно оказывается, что на C# не только быстрее разрабатывать, но и сам код будет производительнее. Просто энтерпрайз такой энтерпрайз.

А ещё энтерпрайзу часто гораздо критичнее падение программы один раз, чем увеличение цены на железо. Энтерпрайз любит стабильность во всём. И тут опять с плюсами не по пути.
Тут было бы интересно притянуть rust, go, D, nim и вот это всё новое и чудесное и посмотреть что генерирует оно, пусть даже на вот этой вот задачи. Из результатов нормальных выводов опять же сделать было бы нельзя, но всё равно любопытно.
Абсолютно согласен. Заголовок — сравнение производительности, на самом же деле сравниваются операции доступа по индексу. Ясень пень, что встроенная проверка на границы массива добавит свой вклад.

Не сравнивается:
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;
}

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

Да и выложенный цикл у вас должен быть не полным перебором, а перебором начиная с i…
Кроме того, после операции items[i]=itemsJ должно поменяться значение, которое в этой программе лежит в itemI (а в оригинале — к нему всегда обращаются как к items[i]).
Блин, ну быстро написал я кусок кода, забыл вставить присваивание j нулю перед внутренним циклом. Идею-то вы поняли или нет?
Я не понял. Как с помощью foreach начать цикл с середины? И какой паттерн для массива правильнее — foreach и счётчик, или for и индекс? Скорее всего, foreach раскроется в тот же for + взятие элемента по индексу.
Да, я быстро глянул в исходный код, не заметив, что внутренний цикл не с нуля, а с i. Но хотя бы внешний for можно поменять на foreach. Идея была такая, что внутренняя реализация foreach должна гарантировать невыход за границы массива, поэтому должна отсутствовать проверка индекса на каждом шаге. Плюс, может быть оптимизирован доступ к очередному элементу массива (в ассемблере для for каждый раз тупо вычисляется с нуля сдвиг в байтах относительно начала массива и потом итоговый адрес). К сожалению, судя по комментам на других сайтах, для рантайма 3.5 включительно, мои предположения неверные. Что странно, вообще говоря. Неужели и в 4.5 ничего не поменялось?
Даже если добавить j=0, не ясно как
foreach(int imemJ in items)
может стать тождественным
for (int j = i; j < ITEMS_COUNT; j++)
пока идея не совсем понятна…
НЛО прилетело и опубликовало эту надпись здесь
Объявление int tmp; за циклом в случае C# экономит время
Не уж то на стеке даже примитивы размещать не умеет?
Похоже что не умеет… Возможно ради безопасности.
Зависит от рантайма. В разных версиях моно оно себя ведет по-разному. Поэтому вынесение переменных вне тела гарантирует единообразное поведение в плане производительности.
О mono в статье ни слова. Точнее есть упоминание, но к тестам это никак не относится.
Тогда это сферические тесты в вакууме, если ограничиваться исключительно одной операционкой и одной версией рантайма. Рантайм для MSIL может быть использован как кросс-платформенное решение и быть переносимым, если знать, что можно использовать, а что не рекомендуется. В комменте выше как раз указывается о разнице в производительности на разных версиях моно для вложенных в блок marshal-by-value типов. Так же есть эпичные фейлы из-за особенностей реализации, например, если попытаться использовать enum-тип в качестве ключа в Dictionary и делать перечисление или вообще любое обращение к данным — будет происходить boxing/unboxing у ключа с выделением и пометкой для GC памяти. Причем если использовать в качестве ключа int и кастовать enum к нему — все работает как на фреймворке от MS. Как сейчас под всякими mono 4.x и тп не знаю, проверялось на 2.8 и 3.0.
А почему std::vector тестировался без reserve() если размер известен?
Спасибо. Да, действительно reserve был бы очень полезен в тесте для std::vector.
Я попробовал его добавить в тест, суммарные результаты получились схожими, и хотя создание вектора стало занимать чуть больше времени, зато заполнение вектора данными стало примерно в 2.5 раза быстрее.
Правда сортировка заняла столько же времени (что наверное логично), поэтому сумма изменилась не существенно.
а мне кажется, что получилось достаточно честно, насколько это вообще можно сравнить… 8)
А может я просто симпатизирую плюсикам))
Какую проблему вы хотите помочь решить? Выбрать c++ или c# до начала реализации проекта? Ну так этот выбор надо делать в зависимости от скилов разработчиков и требований заказчика. Если производительности managed кода окажется недостаточно, во-первых, можно его оптимизировать разными способами, во-вторых, можно реализовать «горячие» методы на c++ или даже на ассемблере в отдельной сборке. Т.е. вообще нет необходимости выбирать что то одно, как вы написали: c# или c++.
В статье я лишь пытаюсь оценить стоимость выбора между С# и С++ с точки зрения производительности. Разумеется, производительность — далеко не единственный критерий для выбора средства разработки, и поэтому, как правило, нельзя делать выбор основываясь исключительно на нем.
пытаюсь оценить стоимость выбора между С# и С++ с точки зрения производительности.

А где методика оценки стоимости? И результаты этой оценки в конкретных цифрах?
Стоимость оценивается исключительно в единицах времени, которое занимает код в runtime.
Методика заключается в измерении этого времени и анализе disassembly являющегося причиной разницы этого времени. (хотя методика, это наверное слишком громко сказано)
Стоимость оценивается исключительно в единицах времени, которое занимает код в runtime.

Тогда это не стоимость, потому что эти единицы времени ничего не значат. Стоимость (для проекта) — это доллары (и прочие деньги).

«Вы потеряете 10-80% производительности» (даже если ваши предположения верны) — это ни о чем. Ну потеряю. Что дальше? Как это повлияет на мой проект?
Это более сложный вопрос. Он за рамками статьи. Есть некоторые соображения на этот счет, но пока они далеки от формализованных. Попробую как-нибудь написать об этом боле развернуто, в виде статьи, когда сформулирую.
Что возвращает нас к вопросу из начала треда: какую проблему вы пытаетесь решить? Какой смысл мерять абстрактные проценты производительности на абстрактных простых задачах?
Я пытаюсь оценить издержки, простые задачи позволяют это сделать и позволяют выполнить анализ причин.
Далеко идущие выводы из одной этой оценки вряд-ли стоит делать, но думаю есть смысл принимать их во внимание при анализе среди прочих факторов.
Вам уже неоднократно объяснили, что простые задачи позволяют оценить издержки на простых задачах. Вопрос их применимости к сложным задачам остается открытым.
По-хорошему это даже не сравнение языков C# vs C++, а сравнение оптимизирующих компиляторов. А если быть точным то сравнение традиционного и JIT компилятора. Посему непонятно, где указаны конкретные версии компиляторов и их настройки?
В статье я писал, что использовал Visual Studio 2010 и настройки по умолчанию для релизной конфигурации (исходники с настройками проекта приложены в статье)

И если быть более точным, то Visual Studio 2010 SP1, С++ компилятор 16.00.40219.01, С# компилятор 4.0.30319.17929.
Понятно, тогда было бы интересно узнать что изменилось за 5 лет и не сократился ли разрыв между ними?
Позже, когда дойдут до этого руки, попробую все перепроверить на 2015 студии.
Скомпилил 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 01322E0A
Спасибо, а как у вас выглядит items[j] = items[i];?

Почему то не вижу в вашем примере кода для
items[j] = items[i];
items[i] = tmp;
а вижу только код для items[i] = tmp;
Тут вся соль оптимизирующего компилятора! Он в начале загнал сравниваемые числа в регистры 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],edx
Ага, понятно, спасибо.
И правда скомпилировал заметно лучше, действительно сильный прогресс в рассмотренном примере.
А проверка выхода за границы массива теперь ушла?
Так же, как понимаю, переменную tmp с оптимизировали до использования регистров?

Вообще конечно надо будет уже скоро переходить на 2015 студию.
Обновление: дело было в неправильном получении disassembly.
На самом деле и в случае VS2010 оптимизация вполне приличная, но все-таки несколько хуже чем в случае с С++.

Тесты на 2015 студии показали разницу порядка 30% на платформе Core i7-3770
С удорожанием стоимости каждого нового % производительности просто чаще будут переходить на модульную разработку — критичные части кода (точнее приложения) писать на том, что легко оптимизировать (в т.ч. и за счет простого выбора языка программирования).
Для этого возьмем код сортировки из самых быстрых примеров и посмотрим во что он компилируется, смотреть будем используя отладчик Visual Studio 2010 и режим disassembly, в результате для сортировки увидим следующий код:

Вы это серьезно? Вообще-то релизный код отличается от кода в режиме отладки.

Зачем вызывать сборщик мусора у дотнета? В реальных условиях он вызывается самостоятельно и редко в нескольких случаях: при превышении какого-то лимита памяти, из-за внешнего воздействия ОС и других. Т.е. искусственно его вызывать не честно.
А вы пробовали вместо 10000 использовать items.Length? В этом случае вроде диапазон выход за границы диапазона не проверяется.

А так вообще бессмысленное сравнение сферического кода в вакууме, из которого следует и так очевидный вывод: «Не используйте .NET в очень редких случаях для числодробилок».
А вы пробовали вместо 10000 использовать items.Length?
Я попробовал, ничего не поменялось в этом случае
Я привел 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

На вид явно остались какие то проверки…
Я привел disassembly релизного кода, а не дебажного.
Как вы получили disassembly релизного кода? Распишите по шагам, пожалуйста. В .NET тут есть нюанс.
Я скомпилировал релизный билд, поставил breakpoint в коде, запустил отладку,
когда выполнение остановилось на breakpoint, я перешел в disassembly из контекстного меню.

Если есть более правильный способ, распишите его пожалуйста тоже.
запустил отладку,
На этом шаге все JIT-оптимизации отключились.

Добавьте перед кодом Console.ReadKey();
Запустите приложение без отладки и, пока оно сидит в ReadKey(), прицепитесь к процессу.
Тогда вы увидите оптимизированный код.
Да, вы правы, тут моя ошибка, реальный оптимизированый код сортировки получается следующий

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

Спасибо, мне нужно обновить
Обновил код в статье и комментарий к нему.

Кстати, если использовать 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
}
}
Тестируйте в релиз режиме, этот ваш дебаг все портит
Я тестирую исключительно в режиме релиз, причем запуская скомпилированый exe отдельно от Visual Studio.
ясно, показалось, значит
а то это распостраненная ошибка измерятелей
НЛО прилетело и опубликовало эту надпись здесь
Из тех, что попадались мне, самой содержательной показалась Head-to-head benchmark: C++ vs .NET

Четырехлетней давности? Серьезно?
Буду рад если дадите ссылки на свежие сравнения, которые вам показались наиболее содержательными.
Просто отдавайте себе отчет в том, что данные оттуда уже вряд ли актуальны.
Да, безусловно актуализация важна. Тесты актуальны для используемых версий Visual Studio и приведенного в них железа, но на свежей Visual Studio и например более свежем железе результаты могут быть другими.
Автор этой статьи тоже использует старьё — Visual Studio 2010
Именно поэтому актуализирую результаты:

Собрал тесты под VS2015 и запустил.
Не вдаваясь в детали: самая быстрая сортировка для C++ заняла 153105, а самая быстрая для C# 206552.

То есть разница порядка 30%
Теперь обработайте NGen и посмотрите что получится.
Тоже самое получается. NGen же влияет только на время запуска ассембли\функции, а весь тест находится в теле одной функции, поэтому к моменту ее запуска уже является скомпилированным.
По идее он ещё умеет инлайнить разные штуки между сборками. Кстати, с инлайном можно поиграть через [MethodImpl(MethodImplOptions.AggressiveInlining)].
Да, но тут тест крайне простой, инлайнить вроде бы и нечего…
Собрал тесты под VS2015 и запустил.

Что за каша? Как можно собирать под студию? С каких пор VS 2015 это платформа?

Вы что, под .NET 4.5 собирали с помощью VS 2015? В чем разница?
Формулирую четче:

Собирал 2015 студией, C# компилятор 1.0.0.50618, C++ компилятор 19.00.23026 .Net Framework 4.6
Пробовал и х86 и х64 сборки, результаты получились схожими:

Самая быстрая C# реализация на 30% медленнее самой быстрой С++ реализации.
Ок, если вы ничего не меняли то под x64 используется RyuJit. Очевидно в данном тесте он выигрыша не дал.

А вообще я присоеденяюсь к тем, кто говорит что тест некорректный. Это даже не синтетический бенчмарк, а непойми что. Сравнивать надо типовые задачи.
Не менял. Выигрыша за рамками погрешности измерений (т.е. за рамками ~3-5%) не было…
Скорее всего я не прав, но надоели эти сравнения двух языков из разных миров и сфер применения. Не утихнет в умах людей академический интерес…
Ну если откровенно, то сферы пересекаются.
Недавно со знакомым мерялись с++ vs java в похожей задаче. Первый оказался примерно в 5 раз быстрее. Использовали стандартные алгоритмы и новые компиляторы. Эти языки для других задач писали.
Первый оказался примерно в 5 раз быстрее

99% что джаву вы меряли криво, ну или не дали ей jit сделать
Я писал реализацию на с++. Не силен в java. Мы замеряли время сортировки, т, е, грубо говоря вызов одного метода. В джаве это Array.sort помоему. Нативные методы тоже на лету компилируются?
И вы конечно же в плюсах тоже использовали тот же алгоритм сортировки, да? (тимсорт).
не удивительно, такие платфформы как java, .net добавляют большой оверхед, Тоесть как правило сравнивать разные инструменты для разных целей нецелесообразно! .net платформа очень много делает за разраба! за универсальность платформы и плюшки приходится платить, меньшей производельностью чем за компилируемые бинарники!
за программы уровня реального времени, приходится платить больше чем программерам которые пишут прикладное ПО, спасибо статья Гууд!!!
Уважаемый автор, посмотрите этот проект если будет интересно. Люди уже заморочились и сделали (куда менее синтетические) тесты, на алогоритмические задачи.
benchmarksgame.alioth.debian.org/u32/compare.php?lang=csharp&lang2=gpp

Да, статья довольно очевидная. C# никто и не выбирает в качестве «умопомрачительной числодробилки».
Спасибо, интересная статья, хотя конечно 15-ти кратное превосходство С++ в тесте regex-dna выглядит немного странным, и версия Mono оставляет некоторые вопросы, но в целом результаты очень интересные, особенно учитывая возможность сравнения с другими языками.
C# быстрее в одних ситуациях, а С++ — в других. Например есть кусок кода, который выполняется 2 раза. Меряем только 1й запуск — С++ быстрее за счёт того, что в С# работал JIT-компилятор. Меряем только второй запуск — C# быстрее за счёт того, что JIT-компилятор уже отработал и оптимизировал код под конкретную платформу. Меряем оба запуска — С++ быстрее за счёт того, что ускорение от оптимизации JIT-компилятора не покрыло расходы на работу JIT-компилятора. Меряем очень много запусков — C# быстрее за счёт того, что оптимизация JIT-компилятора окупилась и принесла дивиденды.
На серверных приложениях, которые перезапускаются раз в сутки или раз в месяц, C# быстрее (как минимум не медленнее). В «линейном» приложении С++ быстрее. В данной статье взяты примеры, которые совершенно не учитывают специфику работы JIT-компилятора, на основании чего сделаны спекулятивные выводы.
Ну и не следует забывать, что C# предоставляет возможности, которых нет в С++.
В некоторых случаях более уместен С (напр., микроконтроллеры), в других — C#, в третьих — javascript.
И, раз уж вы хотите принудительно вызывать сборку мусора через GC.Collect, которая тоже занимает время, то для чистоты эксперимента нужно и в примере на С реализовать менеджера умных ссылок.
Для честности стоило бы делать разогрев, а не выполнять тест сразу с запуска на шарпе. В случае циклов шарп проседает в начале, а потом работает с скоростью C++, так как происходит кэширование инструкций.
И всё-таки gc.collect не эквивалент delete. Сборщик вынужден персчитать и просмотреть все ссылки, когда delete выполняет уже вашу инструкцию.
Можете чуть подробнее описать (в идеале показать на disassembly), какие именно инструкции кэшируются в начале цикла С#? И как именно нужно прогревать С# код?
какие именно инструкции кэшируются в начале цикла С#
Кэшируется всё тело метода.
При первом вызове метода JIT-компилятор компилирует MSIL в нативный код и кэширует его. При последующих вызовах метода исполняется этот нативный код.

Прогреть значит запустить как минимум один раз метод вне бенчмарка, чтобы он скомпилировался.
Либо скомпилировать сборку NGen'ом.

По слухам, для прогрева (в т.ч. на C++) нужно прогнать алгоритм десяток-два раз.
Что-то там про кэш процессора и т.д.
Тут в тонкостях бенчмаркинга я уже, увы, не силён.
Понятно. Конкретно в моем тесте тогда получается прогревать не чего, объясню:
1. Весь код теста находится в одной функции, т.е. она будет уже скомпилирована
2. Третьи функции не вызываются из участков кода, подлежащих измерению.
Иными словами в тесте код уже «прогрет» к моменту начала измерений.

На счет прогрева с целью положить данные в кэш процессора — да понимаю о чем вы говорите. Но тут тесты С++ и С# находятся в равном положении — такой прогрев и там и там отсутствует.
Но тут тесты С++ и С# находятся в равном положении — такой прогрев и там и там отсутствует

Они не в равных условиях, потому что содержимое кеша в обоих тестах будет разным. Соответственно кеш-промахи будут происходит в различные моменты времени даже с идентичным кодом. Все таки ваш код не один на машине (ядре) исполняется и в кеше дофига чужих данных. Вы никогда не думали, почему время выполнения все время плавает? Именно поэтому. Начальные условия всегда разные, параллельно исполняемый код других приложений мешает. Поэтому делаются прогревы и усредняются показатели нескольких тестов.

Вам нужно изучать матчасть, если вы хотите что-то измерять. С такими рассуждениями даже нормальные тесты дадут цифры, которые ничего не значат.
Я конечно же делал несколько тестов, в результате отклонения были до 3-5%. Думаю это отклонение и вызвано наличием промахов по кэшу, и прочих случайных событий. Мне кажется принять его за погрешность измерений достаточно для того чтобы считать результатами достоверными, но с погрешностью.

Также я думаю что промахи по кэшу и прочие условно случайных событий для С++ и для С# будут примерно одинаковыми.
исправил
Причем тут это к статье? Это просто очень криво написанный фраемворк, причем кривой код написан в том числе на плюсах.
Это пример реальной программы, когда из «микро» складывается «макро». Хотелось бы примеров на код, чтобы оценить «кривоту»
нет, это просто кривой код. почему он кривой — почитайте тут. С таким подходом хоть на плюсах, хоть на чем пиши — будет тупить.
Только visual studio на этом же WPF работает отлично и ничем не выдает то, на чем она работает. Это просто кривой софт, написанный на фреймворке, с которым люди не умеют работать. В комментариях, к счастью, это упоминалось некоторыми.
Кстати WPF достаточно спорная по производительности библиотека, например в статье

Сравнение производительности UI в WPF, Qt, WinForms и FLTK

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

Сравнение показало, что датагрид WPF, является одним из худших по производительности, причем на столько что при загрузке данными ентерпрайз уровня дает лишь 12 FPS на железе близком к топовому, а на железе по проще вообще еле шевелится.

Мне кажется знание этого очень полезно разработчикам, решившим использовать WPF датагрид для отображения тяжелых данных. Мне бы такое знание могло бы помочь года три-четыре назад, но к сожалению попадалась только реклама того, наскольо это хороший контрол…

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

И мне кажется, что не я один хочу этого, потому что по результатам опроса, приведенного в статье, большинство ожидает хотя бы 30 FPS от приложений.
причем на столько что при загрузке данными ентерпрайз уровня
«Данные энтерпрайз уровня» вот любят же люди термины выдумывать. Нет никаких «данных энтерпрайз уровня», в enterprise может встречаться хоть 10 строк хоть 10 миллионов, границы нету.
Вы пишете
Кстати WPF достаточно спорная по производительности библиотека, например в статье
Приводя в пример один стандартный контрол (у которого есть несколько сторонних альтернатив к тому же). Очень ценный совет, и очень ценное обобщение конечно же.
Так или иначе количество строк и столбцов в статье написано точно и исходный код выложен.

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

Если знаете быстрый грид для WPF, напишите пожалуйста, ну а если не знаете, то к чему ваш посыл?
Интересно посмотреть вызов функции с этим кодом… часто у языков узким местом является именно вызов функции.
Жду сравнения производительности Ruby vs Assembly.
НЛО прилетело и опубликовало эту надпись здесь
Выделять память под контейнер или массив.
НЛО прилетело и опубликовало эту надпись здесь
Все-таки слово аллокация,- есть в русском языке, хотя и пришло в русский язык из других языков:
http://dic.academic.ru/dic.nsf/dic_fwords/3506/АЛЛОКАЦИЯ
http://dic.academic.ru/dic.nsf/business/605/Аллокация

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

Неважно, откуда пришло. Посмотрите сами ссылки — смысл слова совершенно иной. Как, например, «Признание правильности добавления к счету, последовавшего уже после подачи его» относится к выделению памяти?

Это примерно как в нелепом «мы продаем свою экспертизу». Слово-то «экспертиза» — есть, но употребляется в русском языке только в смысле «была проведена баллистическая экспертиза» и т.п.

allocate — назначать, выделять, распределять, отводить, размещать, располагать в определенном месте.
Есть и толкование «Распределение продукции и производственных мощностей в пространстве рынка.», которое является ближе к выделению памяти.

Но в целом я соглашусь с вами, в русской языке этот термин чаще используется в несколько других целях.
Не углядел ссылку на тестовые файлы…
Не понимаю, каким таким образом удалось получить такую разницу между std::vector и HeapArray. Если в векторе доступ был через оператор индексирования, то разницы с простым массивом быть в принципе не могло (мы же говорим о release версии). Вообще, между всеми четырьмя типами С++ массивов не должно было быть принципиальной разницы, учитывая размер массива. Такое чувство, что разница в результатах обусловлена кэш-миссами и вообще работой с памятью. Прям хочется повторить со всеми четырьмя массивами, но нужны исходники.
В сылка в статье есть, разделе со словами «Остальные же примеры, со вставками для подсчета скорости выполнения,- полностью можно увидеть тут.»

http://www.filedropper.com/performance
Продублирую ссылку еще раз.

С вектором была разница в зависимости от платформы (на новых разницы не было). Возможно повлияли какие-то размеры кэша или же просто различные оптимизации в аппаратной реализации процессора…
Да, в актуальной версии разницы во времени доступа нет — всё ОК.
Очень мило среди пузырьков смотрится результат std::sort.
Попробовал сравнить производительность на функции из реального проекта (одна из самых «числодробительных» частей программы, на некоторых этапах работы в ней проходит 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).


Она занимается распаковкой некоторого битового потока.
Время распаковки 1 ГБ на C# (в том виде, как она написана) — 6.7 сек. Если её переписать на C++, получится 5.4 сек. Если в C# заменить rec и res на указатели, время уменьшится до 5.8 сек, если заменить на указатели ещё и три остальных массива — то получится 5.48 сек, всего на 1.5% больше, чем в C++. Исходный проигрыш был 24%.
Пока эта проигранная секунда не окажется для кого-нибудь критической, переходить из-за одной функции к смешанному коду (C# + unmanaged C++) не буду :)
Сегодня я ещё немного поэкспериментировал с этой сортировкой.
Проверял 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;
                        }
                    }
                }
            }
        }


— массив, без использования переменных перед сравнением, цикл до Array.Length
— массив, с использованием переменных перед сравнением, цикл до Array.Length
— массив, с использованием переменных перед сравнением, цикл до L
— указатели, с использованием индексов
— указатели, с использованием инкремента

Первый же тест показал, что массивы (первый вариант) работали в 5 раз хуже, чем указатели — 17.32 сек на 100 сортировок против 3.67 сек!
Оказалось, что важно, где находится массив: он у меня был описан, как static поле класса.
Поэтому пришлось проверить 5 вариантов массива:
— статическое поле
— поле экземпляра класса
— локальная переменная
— параметр
— статическое поле, скопированное в локальную переменную.

Тесты проводились на двух процессорах — Intel i7, 2.9 GHz (под Windows 10) и ARM Cortex-A9, 1.5 GHz (под Linux, Mono 3.2.8) — ради чего всё и затевалось: нужно проверить быстродействие разных операций в С# на встроенном компьютере. Mono проверялся в двух режимах: с оптимизацией по умолчанию и с --optimize=unsafe (обещают, что в этом режиме отключена проверка индексов).
Результаты такие.
Test                    Intel    ARM    ARM --optimize=unsafe

Static class field:
  Array, no vars        17.31   33.36   19.95
  Array, vars, Length   12.02   21.01   14.74
  Array, vars, L        12.01   19.04   14.32
  Pointer, index         5.76    6.37    6.79
  Pointer, move          3.67    3.02    3.02

Class field:
  Array, no vars         8.57   30.73   17.28
  Array, vars, Length    8.56   23.25   13.92
  Array, vars, L         8.62   18.66   12.24
  Pointer, index         5.74    6.42    5.96
  Pointer, move          3.72    3.31    3.97

Local variable:
  Array, no vars         5.75   22.17    9.82
  Array, vars, Length    5.77   17.24    7.66
  Array, vars, L         6.08   17.05    7.64
  Pointer, index         5.75    5.17    6.39
  Pointer, move          3.68    3.04    3.44

Parameter:
  Array, no vars         5.75   25.12    9.47
  Array, vars, Length    5.79   17.21    7.65
  Array, vars, L         6.11   16.98    7.62
  Pointer, index         6.63    8.04    6.67
  Pointer, move          3.70    4.27    3.96

Copy of static field:
  Array, no vars         5.73   24.86    9.48
  Array, vars, Length    5.78   17.20    7.64
  Array, vars, L         6.07   16.98    7.90
  Pointer, index         5.75    6.79    6.31
  Pointer, move          3.66    3.02    3.95


Надо учитывать, что на Intel замерялось время 100 сортировок, а на ARM — только 10: этот процессор на моих задачах в 10-50 раз медленнее.

Итак, скорость работы с массивами очень сильно зависит от того, где лежит переменная, представляющая этот массив. Разница легко достигает 3 раз (под Windows).
Разница между самым плохим и самым хорошим кодом на ARM (static массив против подвижных указателей, safe mode) — 11 раз! Хотя на первый взгляд код с массивами подозрений не вызывает.
Копирование static массива в локальную переменную увеличивает скорость под Windows и под Mono в unsafe mode в 2-3 раза. На Mono в safe mode выигрыш незначительный.
Unsafe mode не даёт для массивов выигрыша, достаточного, чтобы догнать подвижные указатели. Наверное, операция обращения к a[i] слишком дорогая.
Насколько существенны остальные странности (например, увеличение времени для подвижных указателей для случая параметра в 1.4 раза — на Mono в safe mode), пока непонятно, но разбираться не очень хочется. И так есть, над чем подумать…
Итак, скорость работы с массивами очень сильно зависит от того, где лежит переменная, представляющая этот массив.

Да. Компилятор пытается сделать предположения о том, кто может или не может изменять эту переменную параллельно с вами, и, как следствие, какие проверки надо встроить.
ну и где функция которой вы измеряли?
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Изменить настройки темы

Истории