Сам компилятор C # не сильно изменяет выдаваемый IL в сборке Release. Примечательно, что он больше не генерирует коды операций NOP, которые позволяют вам установить точку останова на фигурной скобке. Большой - это оптимизатор, который встроен в JIT-компилятор. Я знаю, что это делает следующие оптимизации:
Метод встраивания. Вызов метода заменяется введением кода метода. Это большой, он делает доступ к свойствам по существу бесплатно.
Распределение регистров процессора. Локальные переменные и аргументы метода могут храниться в регистре ЦП, и никогда (или реже) не сохраняться обратно в кадр стека. Это большая проблема, которая отличается трудностью отладки оптимизированного кода. И придание изменчивому ключевому слову значения.
Исключение проверки индекса массива. Важная оптимизация при работе с массивами (все классы коллекций .NET используют массив внутри). Когда JIT-компилятор может проверить, что цикл никогда не индексирует массив вне границ, он устраняет проверку индекса. Большой.
Разматывание петли. Циклы с маленькими телами улучшаются, повторяя код до 4 раз в теле и зацикливаясь меньше. Снижает стоимость ветвления и улучшает суперскалярные параметры исполнения процессора.
Устранение мертвого кода. Утверждение типа if (false) {/ ... /} полностью исключается. Это может произойти из-за постоянного складывания и наклона. В других случаях JIT-компилятор может определить, что у кода нет возможных побочных эффектов. Эта оптимизация делает профилирование кода таким сложным.
Подъем кода. Код внутри цикла, на который цикл не влияет, может быть удален из цикла. Оптимизатор компилятора Си будет тратить гораздо больше времени на поиск возможностей для подъема. Тем не менее, это дорогостоящая оптимизация из-за необходимого анализа потока данных, и дрожание не может позволить себе время, поэтому поднимаются только очевидные случаи. Заставлять программистов .NET лучше писать исходный код и самим поднимать.
Устранение общего подвыражения. х = у + 4; z = y + 4; становится z = x; Довольно часто встречается в таких выражениях, как dest [ix + 1] = src [ix + 1]; написано для удобства чтения без введения вспомогательной переменной. Не нужно ставить под угрозу читабельность.
Постоянное складывание. х = 1 + 2; становится х = 3; Этот простой пример обнаруживается компилятором на ранней стадии, но происходит во время JIT, когда другие оптимизации делают это возможным.
Копирование распространения. х = а; у = х; становится у = а; Это помогает распределителю регистра принимать лучшие решения. Это большая проблема в джиттере x86, потому что у него мало регистров для работы. Правильный выбор имеет решающее значение для перфекта.
Это очень важные оптимизации, которые могут иметь большое значение, когда, например, вы профилируете сборку Debug своего приложения и сравниваете ее со сборкой Release. Это действительно имеет значение, хотя, когда код находится на вашем критическом пути, от 5 до 10% кода, который вы пишете, действительно влияет на производительность вашей программы. Оптимизатор JIT не настолько умен, чтобы заранее знать, что критично, он может только применить «поворот на одиннадцать» для всего кода.
Эффективный результат этих оптимизаций для времени выполнения вашей программы часто зависит от кода, который выполняется в другом месте. Чтение файла, выполнение запроса к базе данных и т. Д. Выполнение работы оптимизатором JIT делает полностью незаметным. Хотя это не против :)
Оптимизатор JIT - довольно надежный код, в основном потому, что он был проверен миллионы раз. Чрезвычайно редко возникают проблемы в версии выпуска вашей программы. Это случается однако. У jitters x64 и x86 были проблемы со структурами. Джиттер x86 имеет проблемы с согласованностью с плавающей запятой, приводя к несколько иным результатам, когда промежуточные значения вычисления с плавающей запятой хранятся в регистре FPU с 80-битной точностью вместо усечения при сбросе в память.