Pull to refresh
225
0
Акиньшин Андрей @DreamWalker

Performance Engineer at JetBrains

Send message

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


A?.Foo();

всё равно выглядит лаконичнее, чем какое-нибудь


if (A != null)
{
    A.Foo();
}

Нет, предположения о порядке мы не делаем. Нам просто приходит набор double-чисел, нужно вернуть сумму.
Сортировка — мысль интересная, но не очень хорошая в плане производительности, сумма будет работать долго.
Можно ли что-нибудь придумать для произвольных входных данных, чтобы работало за O(N) ?

@PsyHaSTe, направление мысли правильное. А можно что-нибудь сделать без модификации входных данных? Другими словами, нужно написать собственную функцию Sum, которая для любого набора чисел выдаст «хорошую» сумму.

Да не пытаюсь я Груви обидеть, не волнуйся так. =) Чудный язык, в нём очень давно появилось много разного и хорошего, с этим никто не спорит. Я просто хотел сказать, что это абсолютно нормально, когда хорошие фичи переходят из одних языков в другие.

Если фича хорошая, то она часто появляется сразу в нескольких хороших языках программирования. А null propagation operator — хорошая фича, поэтому мы можем её наблюдать не только в C#6 и Kotlin, но ещё и в Groovy. =)

Это очень клёвая фича. Представьте, что у вас есть некое свойство A, у которого есть свойство B, у которого есть свойство C, у которого есть свойство D, у которого вы хотите вызвать метод Foo. Но вот проблема: по пути к методу Foo одно из свойств A, B, C, D может оказаться равным null. Раньше приходилось писать:


if (A != null && A.B != null && A.B.C != null && A.B.C.D != null)
    A.B.C.D.Foo();

В C#6 вы можете написать просто


A?.B?.C?.D?.Foo();

Материал для дополнительного чтения:


Я гонял Райдер на Райдере. Работает шустренько, не жалуемся.
Ок, я подумаю, что с этим можно сделать.
По поводу Issues. #97 уже сделан в develop ветке, #96 скоро сделаю. Постараюсь выложить новую версию в NuGet на этих выходных. Помимо прочего, у нас там идёт работа над поддержкой DNX и CoreCLR, что занимает много времени.
По поводу вашего случая: если у вас получится придумать удобный API который бы покрывал ваш случай и не мешал бы работать обычным бенчмарком, то я с удовольствием включу его в библиотеку. Но лично я пока плохо представляю, как такой функционал следовало бы реализовать. Было бы чудно, если бы вы накидали в виде кода то, как вы всё это видите (+ желаемый вид summary-таблички).
Ну, выдирать имя функции из тела — это достаточно частная задача, не думаю, что имеет смысл включать её в ядро библиотеки. В вашем случае я бы предложил использовать TagColumn для формирования дополнительных колонок из названий методов, см. IntroTags. Свой Baseline будет писаться очень просто: если нам дали Custom-класс, то возвращаем в новой чудо-колонке 1, а если Linq, то находим бенчмарк с такой же функцией в Custom-случае и считаем отношение.

Если по мере подготовки статьи будут возникать какие-то дополнительные вопросы/замечания/предложения, то не стесняйтесь обращаться. В BenchmarkDotNet мне хочется сделать такой API, который можно действительно легко адаптировать под любые кастомные задачи.
А есть возможность сделать так, чтобы все методы были в интерфейсе, который реализуется в каждом из классов? Если да, то проще всего было бы завести параметр, который бы определял имплементацию. Ну, и в каждом бенчмарк-методе использовать этот параметр, чтобы использовался бы инстанс нужного класса. И можно было бы легко написать свой BaselineColumn, который сравнивает первую имплементацию со второй. Если это не нанобенчмарки (на метод уходит хотя бы 50ns), то накладными расходами на выбор имплементации вполне можно пренебречь.

Очень удобно, когда функция показана в отдельном стобце, а не кодируется в названии метода LinqMax, CustomMax и т.п.

А можете привести пример того, как вы это себе представляете?
  1. На текущий момент экспорт в csv делается всегда, а через ManualConfig можно его выключить.
  2. Сейчас Baseline выбирается на группу параметров. Т.е. если у меня есть [Params(1,2,3)] int X, то на каждое значение X будет собственный Baseline. Если вы хотите сравнивать A1 с A2, а B1 с B2 попарно, то нужно делать два отдельных класса. Группировку методов сделать не так сложно, но сложно сделать так, чтобы для общего случая результаты отображались бы так, чтобы всем было понятно, к чему какой Baseline-относится (если у вас есть хорошие идеи, то я их с радостью послушаю).
    Но если очень хочется, то всегда можно прописать собственный IColumn по аналогии с BaselineDiffColumn и добавить в конфиг (кода нужно совсем немного, зато вы сможете заточить колонку под собственные нужды).
Я устал с вами спорить, это мой последний комментарий.

Рассказываю последний раз: вы написали некоторую программу на 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.
Получается мой тест оценивает цену этой проверки с точки зрения производительности, разве это не полезно?

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

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

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

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

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

Я постараюсь подвести итог нашей дискуссии. В комментариях к этому посту много людей старалось вам объяснить, что ваш бенчмарк не является корректным, а результаты не несут особой значимости и полезности. Вам дали много хороших советов о том, как следует подходить к рассмотрению подобных тем. Настоятельно рекомендую внимательно всё просмотреть, изучить соответствующий материал и использовать новые знания для своих следующих работ.
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#).
Сравнение C# и C++ по производительности — очень странная тема.
1. Я считаю, что некорректно сравнивать производительность языков. У языков нет производительности. Вы можете сравнивать только код, который выдаёт конкретный компилятор C++, с кодом, который выдаёт конкретный компилятор C# (который запускается по конкретной версией .NET-рантайма с конкретным JIT-компилятором). Нельзя взять какую-то одну конфигурацию для запуска, на основе которой делать выводы про язык.
2. В своём посте вы пытаетесь сравнивать C++ программу и C# программу, которые похожи внешне, не задумываясь о том, что items[i] в C++ и C# означают разные вещи.
3. Я считаю, что если уж сравнивать разные языки программирования по скорости, то нужно брать задачу и пытаться решить её максимально эффективно на каждом из языков, после чего проанализировать: насколько сложным получились наиболее эффективные решения, насколько быстро они работают.
4. Если хорошо понимать методику работы конкретного JIT + понимать как работает CPU, то на C# вполне можно добиться высокой эффективности для конкретного небольшого метода, получится не хуже, чем в C++. В споре C++ vs C# самое интересное для обсуждения — накладные расходы от самого рантайма (например, GC), эффективность работы стандартный классов, которыми все пользуются, и т. п.

Это прекрасно, что вы заинтересовались темой производительности. Я считаю, что подобные эксперименты помогут вам лучше разобраться с тем, что находится под капотом ваших программ. Но если вы решаете опубликовать свои наработки (например, на Хабре), постарайтесь удостовериться, что материал несёт в себе полезную информацию, которая основывается на хороших экспериментах и может принести пользу другим программистам.
Вы игнорируете мои основные тезисы. Нет смысла делать дополнительные прогоны, т. к. в бенчмарке слишком много других проблем, я писал о них выше.
Сортировка — очень плохой пример для замеров эффективности чтения/записи. Вы прибавляете к этой операции кучу сайд-эффектов типа промахов кэша, итерации по массиву и т. п.
Если вы делаете академический эксперимент по измерению эффективности одной конкретной операции, то измеряйте одну конкретную операцию.
Если вы хотите решить реальную задачу, то решайте реальную задачу, а не делайте выводы о языке на основе n^2-сортировке на managed-коде. В вашем бенчмарке кроется огромное количество слабых мест, о которых вы ни строчке не написали.
Не, unsafe-пузырёк я могу написать. Другой вопрос в том: зачем? Что именно вы хотите измерить? Без ответа на этот вопрос остальной разговор имеет мало смысла. Если вы хотите померить производительность операций чтения/записи над массивам, то сортировка вам не нужна. Если вы хотите померить то, насколько хорошо работает for, то сортировка вам тоже не нужна.
Если же вы хотите понять, насколько производительную сортировку можно написать на каждом из языков, то вам не подходят примитивные сортировки. Брать сортировку за N^2 и добиваться от неё хорошей производительности — странное академическое упражнение.
Сложность заключается как раз в выборе типа сортировки + особенностей её реализации под unsafe.
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. У вас есть раздел «Выводы», но подобных фраз там нет.

Information

Rating
Does not participate
Works in
Registered
Activity