Каждый C#-разработчик знает, что C#-компилятор переводит исходный код программы в промежуточный язык под названием Intermediate Language (IL). А за превращение IL в последовательность машинных команд чаще всего отвечает Just-In-Time-компилятор (JIT). Да, на сегодняшний день есть NGen, Mono AOT, .NET Native, но JIT-компиляция всё ещё лидирует в мире .NET-приложений. А вот работает этот самый JIT, знают далеко не все. Если брать в расчёт только реализацию .NET от Microsoft, то стоит различать JIT-x86 и JIT-x64. А ещё за дверями стоит RyuJIT который уже совсем скоро займёт почётное место основного JIT-компилятора. А если вы любите старые версии .NET, то полезно знать, что в разных версиях CLR логика работы JIT отличалась. Исходники у нас теперь открыты, вы можете их посмотреть и осознать, насколько же это большая и сложная тема. Сегодня мы не будем пытаться охватить её, а лишь кратко посмотрим на несколько интересных особенностей отдельных версий JIT-компиляторов. Итак, сегодня в номере:
- Почему короткий метод может не быть заинлайнен и как этого избежать
- JIT-баги: опасные и беспощадные
- Кто и как разматывает циклы
- Чем отличается размотка маленьких и больших циклов
JIT-x86 и starg
Откроем исходник конструктора Decimal
с параметром типа int
из .NET Reference Source:
// Constructs a Decimal from an integer value. // public Decimal(int value) { // JIT today can't inline methods that contains "starg" opcode. // For more details, see DevDiv Bugs 81184: x86 JIT CQ: Removing the inline striction of "starg". int value_copy = value; if (value_copy >= 0) { flags = 0; } else { flags = SignMask; value_copy = -value_copy; } lo = value_copy; mid = 0; hi = 0; }
Заинтригованы? А всё дело в том, что JIT-x86 не умеет инлайнить методы, в IL-коде которых содержатся инструкции starg
или ldarga
. Decimal-конструктор очень желательно заинлайнить, поэтому разработчики стандартного класса пошли на хитрость: скопировали параметр в локальную переменную, чтобы избежать «плохой» инструкции. В JIT-x64 эту «фичу» убрали. Для заинтересовавшихся рекомендуется к изучению:
- История про инлайнинг под JIT-x86 и starg
- .NET Reference Source: Constructs a Decimal from an integer value
- CoreCLR, JIT sources: flowgraph.cpp (Feb 26, 2015)
- CoreCLR, JIT sources: importer.cpp (Feb 26, 2015)
- MSDN: starg
- MSDN: ldarga
- Stackoverflow: .NET local variable optimization
Странный баг в JIT-x64
Уважаемые знатоки, внимание, вопрос: что выведет следующий код для step=1
?
private int bar; public void Foo(int step) { for (int i = 0; i < step; i++) { bar = i + 10; for (int j = 0; j < 2 * step; j += step) Console.WriteLine(j + 10); } }
Правильный ответ: зависит. Скорее всего вы ожидаете увидеть 10 11
, но баг в оптимизации JIT-x64 всё испортит и даст нам 10 21
. В JIT-x86 и RyuJIT всё работает хорошо. С багом придётся смириться, Microsoft не хочет его исправлять. Пример очень хрупкий, наткнуться на него в реальной жизни крайне проблематично. Кто-то спросит: но если это редкий баг, то зачем про него знать? Зачем вообще интересоваться подобными штуками? Если вы человек весёлой натуры, то можно использовать баг в своих целях. Например, определить в рантайме какая версия JIT сейчас используется:
public enum JitVersion { Mono, MsX86, MsX64, RyuJit } public class JitVersionInfo { public JitVersion GetJitVersion() { if (IsMono()) return JitVersion.Mono; if (IsMsX86()) return JitVersion.MsX86; if (IsMsX64()) return JitVersion.MsX64; return JitVersion.RyuJit; } private int bar; private bool IsMsX64(int step = 1) { var value = 0; for (int i = 0; i < step; i++) { bar = i + 10; for (int j = 0; j < 2 * step; j += step) value = j + 10; } return value == 20 + step; } public static bool IsMono() { return Type.GetType("Mono.Runtime") != null; } public static bool IsMsX86() { return !IsMono() && IntPtr.Size == 4; } }
Материал для дополнительного чтения:
- История про баг в JIT-x64
- Определение версии JIT в рантайме
- StackOverflow: JIT .Net compiler bug?
- MS Connect: x64 jitter sub-expression elimination optimizer bug
- StackOverflow: How to detect which .NET runtime is being used (MS vs. Mono)?
- StackOverflow: How do I verify that ryujit is jitting my app?
Размотка циклов
Размотка циклов — это такая очень хорошая оптимизация, которую любят делать многие компиляторы. Суть в том, что мы заменяем цикл вида
for (int i = 0; i < 1024; i++) Foo(i);
на
for (int i = 0; i < 1024; i += 4) { Foo(i); Foo(i + 1); Foo(i + 2); Foo(i + 3); }
Помимо сокращение количества операций инкремента, мы имеем улучшенные условия для дополнительных операций на уровне процессора (например, branch prediction и instruction-level parallelism). Увы, JIT-x86 и RyuJIT среднестатистический цикл разматывать не особо умеют. А вот JIT-x64 иногда умеет, хоть и делает это в своей особой манере. Например, если количество итераций делится на 2 или 3, то код
int sum = 0; for (int i = 0; i < 1024; i++) sum += i; Console.WriteLine(sum);
превратится во что-то вида
int sum = 0; 00007FFCC8710090 sub rsp,28h for (int i = 0; i < 1024; i++) 00007FFCC8710094 xor ecx,ecx 00007FFCC8710096 mov edx,1 ; edx = i + 1 00007FFCC871009B nop dword ptr [rax+rax] 00007FFCC87100A0 lea eax,[rdx-1] ; eax = i sum += i; 00007FFCC87100A3 add ecx,eax ; sum += i 00007FFCC87100A5 add ecx,edx ; sum += i + 1 00007FFCC87100A7 lea eax,[rdx+1] ; eax = i + 2 00007FFCC87100AA add ecx,eax ; sum += i + 2; 00007FFCC87100AC lea eax,[rdx+2] ; eax = i + 3 00007FFCC87100AF add ecx,eax ; sum += i + 3; 00007FFCC87100B1 add edx,4 ; i += 4 for (int i = 0; i < 1024; i++) 00007FFCC87100B4 cmp edx,401h 00007FFCC87100BA jl 00007FFCC87100A0
Это достаточно важная информация. Например, многие предвкушают переход с JIT-x64 на RyuJIT, ведь Microsoft обещают нам много вкусного: поддержку SIMD и ускоренную JIT-компиляцию. А вот про производительность самого кода они как-то молчат. Нужно понимать, что отсутствие некоторых оптимизаций в RyuJIT (по сравнению с JIT-x64) может немножко сократить скорость работы вашей программы. Полезные ссылки:
- RyuJIT CTP5 и размотка циклов
- Википедия: Размотка цикла
- Wikipedia: Loop unrolling
- J. C. Huang, T. Leng, Generalized Loop-Unrolling: a Method for Program Speed-Up (1998)
- Википедия: Предсказывание переходов (branch prediction)
- Википедия: Параллелизма уровня команд (instruction-level parallelism)
- Wikipedia: Inline expansion
- Wikipedia: Cache miss
- StackOverflow: http://stackoverflow.com/questions/2349211/when-if-ever-is-loop-unrolling-still-useful
- Blogs.Msdn: RyuJIT: The next-generation JIT compiler for .NET
Больше интересных JIT-багов
Вот вам ещё задачка:
struct Point { public int X; public int Y; } static void Print(Point p) { Console.WriteLine(p.X + " " + p.Y); } static void Main() { var p = new Point(); for (p.X = 0; p.X < 2; p.X++) Print(p); }
Данный цикл также можно раскрутить. Итерации всего две, так что от условных переходов можно избавиться вовсе: достаточно повторить тело цикла дважды. Занимательный факт: в CLR2 JIT-x86 была бага, которая портила жизнь и вместо 0 1 1 0
выдавала 2 0 2 0
. Наткнуться на неё не так уж и сложно. Благо, в CLR 4 её поправили, а в других версиях JIT её и вовсе не было. Имейте ввиду, что если вы работаете под .NET Framework 3.5 (да, некоторым всё ещё приходится), то это подразумевает CLR2. Нужно быть готовыми, что такой простой код превратится в
var p = new Point(); 05C5178C push esi 05C5178D xor esi,esi ; p.Y = 0 for (p.X = 0; p.X < 2; p.X++) 05C5178F lea edi,[esi+2] ; p.X = 2 Print(p); 05C51792 push esi ; push p.Y 05C51793 push edi ; push p.X 05C51794 call dword ptr ds:[54607F4h] ; Print(p) 05C5179A push esi ; push p.Y 05C5179B push edi ; push p.X 05C5179C call dword ptr ds:[54607F4h] ; Print(p) 05C517A2 pop esi 05C517A3 pop edi 05C517A4 pop ebp 05C517A5 ret
А вообще, тема размотки маленький циклов представляет особый интерес. В то время, как JIT-x86 любит их разматывать (это большой цикл размотать сложно, а вот с маленьким всё намного проще), RyuJIT (который основан на кодовой базе 32-битного JIT) разматывать их отказывается. А вот JIT-x64 тут нас может порадовать. Скажем, он может взять код
int sum = 0; for (int i = 0; i < 4; i++) sum += i; Console.WriteLine(sum);
и предподсчитать значение:
int sum = 0; 00007FFCC86F3EC0 sub rsp,28h Console.WriteLine(sum); 00007FFCC86F3EC4 mov ecx,6 ; sum = 6 00007FFCC86F3EC9 call 00007FFD273DCF10 00007FFCC86F3ECE nop 00007FFCC86F3ECF add rsp,28h 00007FFCC86F3ED3 ret
Но не надо думать, что RyuJIT во всём хуже JIT-x64. Да, с оптимизациями в JIT-компиляторе нового поколения всё не так хорошо, но зато в среднем по больнице код получается более вменяемый. Узнать больше про размотку маленьких циклов можно тут:
Хотите знать больше про внутренности .NET?
Тогда заходите к нам на огонёк! В скором времени в Москве (03–04 апреля), Екатеринбурге (17 мая) и Санкт-Петербурге (29–30 мая) пройдёт серия семинаров CLRium #2 (онлайн трансляция включена). Будем обсуждать будущее .NET: поговорим про анатомию нового CoreCLR, особенности RyuJIT, хардкорные примеры по работе с Roslyn и потроха CoreFx! Нескончаемый поток интересных и полезных знаний поможет вам не только намного лучше понять как работают ваши собственные C#-программы, но и подготовит к светлому .NET-будущему, в котором вы сможете использовать силу платформы на полную!
ссылка на оригинал статьи http://habrahabr.ru/post/252105/
Добавить комментарий