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