Ответ 1
Вы должны посмотреть машинный код, который генерирует дрожание, чтобы увидеть основную причину. Используйте "Инструменты" > "Параметры" > "Отладка" > "Основные" > отключить параметр "Запретить оптимизацию JIT". Переключитесь на сборку Release. Установите точку останова на первом и втором циклах. Когда он работает, используйте Debug > Windows > Disassembly.
Вы увидите машинный код для тел цикла for:
sum1 += i;
00000035 add esi,eax
и
sum2 += i;
000000d9 add dword ptr [ebp-24h],eax
Или, другими словами, переменная sum1
хранится в регистре CPU esi
. Но переменная sum2
хранится в памяти в кадре стека метода. Большая разница. Регистры очень быстры, память медленная. Память для фрейма стека будет находиться в кеше L1, а на современных машинах доступ к этому кешу имеет задержку в 3 цикла. Буфер хранилища будет быстро перегружен большим количеством операций записи, что приведет к остановке процессора.
Поиск способа хранения переменных в регистре CPU является одной из основных обязанностей по оптимизации дрожания. Но у этого есть ограничения, x86, в частности, имеет мало регистров. Когда все они израсходованы, джиттер не имеет опции, но вместо этого использует память. Обратите внимание, что оператор using
имеет дополнительную скрытую локальную переменную под капотом, поэтому он имеет эффект.
В идеале оптимизатор джиттера сделал бы лучший выбор, как распределять регистры. Используя их для переменных цикла (что он и сделал) и сумма переменных. Компилятор с опережением времени получит это право, имея достаточно времени для выполнения анализа кода. Но компилятор "точно в срок" работает под строгими временными ограничениями.
Основные контрмеры:
- Разделите код на отдельные методы, чтобы можно было повторно использовать регистр, такой как ESI.
- Удалите форсирование дрожания (Project > Properties > вкладка Build > untick "Предпочитаете 32-битный" ). x64 содержит 8 дополнительных регистров.
Последняя пуля эффективна для устаревшего джиттера x64 (целевой .NET 3.5 для его использования), но не для переписывания джиттера x64 (aka RYuJIT), впервые сделанного в 4.6. Переписывать, что было необходимо, потому что унаследованный джиттер потребовал слишком много времени для оптимизации кода. Разочаровав, RyuJIT действительно умеет разочаровываться, я думаю, что его оптимизатор мог бы сделать здесь лучшую работу.