
Комментарии 46
Не знаю, как у вас там в С#, но для микроконтроллеров в классическом СИ в любой приличной IDE (IAR, KEIL, и т.д.) компиляторы имеют несколько режимов оптимизации уже давным-давно. И как раз такие вещи они прекрасно обрабатывают. Даже могут несколько строк кода выбрасывать из прошивки, если эти строки не имеют смысла. Во время отладки с непривычки разработчик может быть удивлён тем, что отладчик пропускает некоторые строки и даже блоки кода типа простых циклов со счётчиком, который нигде больше не используется (например, для задержки на 1 млн тактов).
Уверен, что для С# оптимизация настраивается от и до.
Уверен, что для С# оптимизация настраивается от и до.
Нет. Это по многим причинам не так. По большому счёту основная разница идёт между debug и release режимами - между ними разница значительна и, конечно, тестирование производительности в debug не стоит выполнять.
Компиляция C#->IL почти не настраивается и почти не оптимизирует (ну разве что на уровне вывода типов немного). Это, похоже, принципиальный подход.
JIT (IL в машинный код) тоже особо не настраивается - это скорее нужно для отладки самого движка .Net, чем для прикладных программистов. Причём JIT ограничен в оптимизациях - во-первых жёсткие требования к времени компиляции, во-вторых некоторые вещи нельзя оптимизировать "как в C" потому что нельзя по контракту.
В итоге .NET может выявить какие-то константные вычисления, некоторые peephole оптимизации, ограниченно может заинлайнить, может выкинуть неисполнимые ветки кода. Но, например, не делает оптимизацию tail recursion - скорее всего для сохранения возможности размотки стека при исключениях, возможно по этой же причине хвостовой call никогда не преобразует в jump. Расчёт констант и выражений, оптимизация циклов и глубина инлайнинга тоже очень далека от -O3 С/С++. Но не скажу, что всё совсем плохо (пользуясь случаем, скажу спасибо @Nagg за его вклад в дело оптимизации движка).
Важно отметить, что это не "C# плохой", это скорее "C# другой".
Но, например, не делает оптимизацию tail recursion - скорее всего для сохранения возможности размотки стека при исключениях, возможно по этой же причине хвостовой call никогда не преобразует в jump.
Вообще говоря, инструкция tail. в IL есть. Другое дело, что компилятор C# очень тяжело заставить её применять. А вот F# и Nemerle активно используют хвостовую рекурсию, если это возможно (например, нет обработки исключений с вызовом рекурсивной функции внутри).
Ну что же, век живи - век учись. Спасибо за наводку. Я не знал о её существовании. В любом случае, C/C++ компилятору проверить допустимость хвостовой рекурсии обычно проще (и поэтому она происходит).
Иногда под дотнетом очень интересные оптимизации встречаются. Например, тот же F# часто вообще рекурсию в цикл преобразует.
Пример с факториалом
Было:
let fact n =
let rec fact_r n acc =
match n with
| 1 -> acc
| n -> fact_r (n - 1) (acc * n)
if n > 1 then fact_r n 1 else 1Стало:
_.fact_r@2(Int32, Int32)
L0000: push ebp
L0001: mov ebp, esp
L0003: lea eax, [ecx-1]
L0006: test eax, eax
L0008: ja short L000e
L000a: mov eax, edx
L000c: pop ebp
L000d: ret
L000e: imul edx, ecx
L0011: mov ecx, eax
L0013: jmp short L0003
_.fact(Int32)
L0000: cmp ecx, 1
L0003: jle short L0010
L0005: mov edx, 1
L000a: call _.fact_r@2(Int32, Int32)
L000f: ret
L0010: mov eax, 1
L0015: retЭквивалентный код на C#:
internal static int fact_r@2(int n, int acc)
{
while (true)
{
if (n == 1) return acc;
acc *= n;
n = n - 1;
}
}
public static int fact(int n)
{
if (n > 1) return fact_r@2(n, 1);
return 1;
}Только не инструкция, а префикс инструкции.
Вот только в своё время изменение обработки этого префикса в JIT-компиляторе вызвало некоторое удивление у некоторых людей.
Не скажу за детали, но суть была в том, что некоторый человек ориентировался на то, что tailcall игнорируется и некоторый ресурсоёмкий сервис в облаке падает со stack overflow, а после обновления рантайма он внезапно стал использовать хвостовую рекурсию и не падал, а пользователю за работу сервиса был выставлен немалый счёт (это в телеге в pro.net как-то обсуждалось)
Хотелось бы увидеть какой-то тест, не один год в C#, никогда не замечал подбного.
Так как JIT о C# ничего не знает, это надо на iL смотреть.
Я думаю было бы хорошо иметь полный пример
... и желательно на https://sharplab.io/
Наличие этой разницы уже многое говорит
Ни о чём не говорит. Это даже на синтетическом примере не даст и 3% разницы.
Да, упустил и не проверил внимательно - как раз к тому, что от небольшой семантически значимой разницы может измениться код. Это отличие было в и коде комментария. Там еще и j определена была.
Я пишу без ООП, короткие программы, я не разработчик, проверяю какие-то алгоритмы и т.п. и для меня такое поведение — нонсенс,
Возможно, вы просто не умеете замерять производительность.
Да, измерять производительность не так просто. Это не про "искривление пространства-времени". Судя по разнице и кейсу, скорее всего у вас разница проявляется на debug режиме. Могу ошибаться, но вы же так и не предоставили конкретные кейсы и замеры. И в этом случае у вас вашим бегунам надо одному после каждых 10 метров отмечаться в бланке о пробегании 10 метров, а второму - раз в 50 метров. В такой процедуре уже неважно, чем вы измеряли - фотофинишем или ручным секундомером, потому что скорость бега на 400 метров вы не измеряете.
База с которой в принципе можно начинать замерять производительность - поставить максимально в одинаковые "стандартные" условия измеряемые кейсы (в .NET стал стандартным инструмент BenchmarkDotNet, в JVM - JMH). Следующая часть - зафиксировать и описать окружение. Следующая - сформулировать гипотезу (в данном случае - что однострочное объявление лучше многострочного). Потом - проверить гипотезу. Потом, если она подтверждается замерами - максимально изолировать причину и уменьшить влияние других факторов. По результатам уже можно было бы поднять issue в проекте dotnet runtime.
Да, бывают какие-то отклонения от этой процедуры, например, если бага в инлайнинге jit, то она как раз НЕ проявляется в debug, а проявляется в release (я такой кейс находил). Но это всё равно примерно тот же сценарий (только код был одинаковый, а окружения отличались ровно на опцию debug/release).
Так полный код предоставьте, на котором такое поведение наблюдается, иначе это все равно голословно.
2) Мы не в суде, я сообщил информацию к сведению, вы можете её проигнорировать
Ну я, в свою очередь, постарался донести свое мнение до других пользователей, которые прочитают ваше сообщение.
1 лучше почисти метрики от контекста, часто nda содержит размытое определение выдачи конфиденциальной информации
У вас не идентичный код, что выше привели вы. У вас не только изменение в объявлении, но и само присваивание для eof совершенно подругому описано. Ваша проблема может состоять в том, что для вычисления eof переменная S уже ушла из кеша процессора например
Неужели все это стоит учитывать?
Голословное утверждение, ничем не подкрепленное.
Однако оба эти подхода привели бы к критическому изменению, поскольку этот метод является частью публичного API.
Разве сейчас контракт функции не поменялся (пусть и не так заметно)? До этого возвращался reference на копию, а сейчас на оригинал. Гипотетически можно представить клиента, который менял полученное правило и при этом никак не влиял на других клиентов. Теперь это изменится
А нельзя просто написать r[i].Match(...)? Зачем это переменная вообще нужна?
Конечно, если как выше написано в комментарии, чтобы отдать копию, то тогда автор статьи рукожоп (не переводчик, переводчик молодец).
Как сделать программу на Go быстрее на 42%, изменив один символ