Почему добавление локальных переменных делает код .NET медленнее
Почему комментирует первые две строки этого цикла for и раскомментирует третий результат при ускорении на 42%?
int count = 0;
for (uint i = 0; i < 1000000000; ++i) {
var isMultipleOf16 = i % 16 == 0;
count += isMultipleOf16 ? 1 : 0;
//count += i % 16 == 0 ? 1 : 0;
}
За временем очень сильно отличается код сборки: 13 против 7 инструкций в цикле. Платформа - это Windows 7 с .NET 4.0 x64. Оптимизация кода включена, и тестовое приложение запускалось за пределами VS2010. [ Обновление: Проект Repro, полезный для проверки настроек проекта.]
Устранение промежуточной булевой является фундаментальной оптимизацией, одной из самых простых в моей эре 1980 года Книга Дракона. Как оптимизация не применялась при создании CIL или JITing машинного кода x64?
Есть ли "действительно компилятор, я бы хотел, чтобы вы оптимизировали этот код, пожалуйста" переключитесь? В то время как я сочувствую настроениям, что преждевременная оптимизация сродни любви любви, я мог видеть разочарование в попытке профилировать сложный алгоритм, который такие проблемы были разбросаны по всей его рутине. Вы будете работать через горячие точки, но не имеете никакого намека на более широкий теплый регион, который может быть значительно улучшен путем ручной настройки того, что мы обычно принимаем как должное из компилятора. Надеюсь, что я что-то пропустил.
Обновление: Разница скорости также возникает для x86, но зависит от порядка, который методы компилируются точно. См. Почему порядок JIT влияет на производительность?
Код сборки (по запросу):
var isMultipleOf16 = i % 16 == 0;
00000037 mov eax,edx
00000039 and eax,0Fh
0000003c xor ecx,ecx
0000003e test eax,eax
00000040 sete cl
count += isMultipleOf16 ? 1 : 0;
00000043 movzx eax,cl
00000046 test eax,eax
00000048 jne 0000000000000050
0000004a xor eax,eax
0000004c jmp 0000000000000055
0000004e xchg ax,ax
00000050 mov eax,1
00000055 lea r8d,[rbx+rax]
count += i % 16 == 0 ? 1 : 0;
00000037 mov eax,ecx
00000039 and eax,0Fh
0000003c je 0000000000000042
0000003e xor eax,eax
00000040 jmp 0000000000000047
00000042 mov eax,1
00000047 lea edx,[rbx+rax]
Ответы
Ответ 1
Вопрос должен быть "Почему я вижу такую разницу на моей машине?". Я не могу воспроизвести такую огромную разницу в скорости и подозреваю, что в вашей среде есть что-то конкретное. Очень сложно сказать, что это может быть. Могут быть некоторые (компиляторы) параметры, которые вы установили некоторое время назад и забыли о них.
Я создаю консольное приложение, перестраиваю в режиме Release (x86) и запускаю VS. Результаты практически идентичны, 1,77 секунды для обоих методов. Вот точный код:
static void Main(string[] args)
{
Stopwatch sw = new Stopwatch();
sw.Start();
int count = 0;
for (uint i = 0; i < 1000000000; ++i)
{
// 1st method
var isMultipleOf16 = i % 16 == 0;
count += isMultipleOf16 ? 1 : 0;
// 2nd method
//count += i % 16 == 0 ? 1 : 0;
}
sw.Stop();
Console.WriteLine(string.Format("Ellapsed {0}, count {1}", sw.Elapsed, count));
Console.ReadKey();
}
Пожалуйста, у кого есть 5 минут, скопируйте код, перестройте, запустите VS и опубликуйте результаты в комментариях к этому ответу. Я бы хотел не сказать "это работает на моей машине".
ИЗМЕНИТЬ
Чтобы убедиться, что я создал приложение 64-разрядное Winforms, и результаты аналогичны результатам в вопросе. первый метод медленнее (1,57 с), чем второй (1.05 сек). Разница, которую я наблюдаю, составляет 33% - еще много. Кажется, есть ошибка в .NET4 64-битном JIT-компиляторе.
Ответ 2
Я не могу говорить с компилятором .NET или его оптимизацией или даже КОГДА он выполняет свои оптимизации.
Но в этом конкретном случае, если компилятор сбрасывал эту логическую переменную в фактический оператор, и вы должны были попробовать и отлаживать этот код, оптимизированный код не будет соответствовать коду как написанному. Вы не сможете сделать один шаг над назначением isMulitpleOf16 и проверить его значение.
Это просто пример того, где оптимизация может быть отключена. Могут быть и другие. Оптимизация может произойти во время фазы загрузки кода, а не фазы генерации кода из CLR.
Современные среды выполнения довольно сложны, особенно если вы используете JIT и динамическую оптимизацию во время выполнения. Я чувствую благодарность, что код делает то, что он говорит, иногда.
Ответ 3
Это ошибка в .NET Framework.
Ну, на самом деле я просто размышляю, но я представил отчет об ошибке Microsoft Connect, чтобы узнать, что они говорят. После того, как Microsoft удалила этот отчет, я повторно отправил его в проект roslyn на GitHub.
Обновление: Microsoft перенесла проблему в проект coreclr. Из комментариев по этой проблеме, называя это ошибкой, кажется немного сильным; это скорее недостающая оптимизация.
Ответ 4
Я думаю, что это связано с другим вопросом. Когда я меняю свой код следующим образом, выигрывает многострочная версия.
oops, только на x86. На x64 многострочная линия является самой медленной, и условное выражение их обоих удобно.
class Program
{
static void Main()
{
ConditionalTest();
SingleLineTest();
MultiLineTest();
ConditionalTest();
SingleLineTest();
MultiLineTest();
ConditionalTest();
SingleLineTest();
MultiLineTest();
}
public static void ConditionalTest()
{
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
int count = 0;
for (uint i = 0; i < 1000000000; ++i) {
if (i % 16 == 0) ++count;
}
stopwatch.Stop();
Console.WriteLine("Conditional test --> Count: {0}, Time: {1}", count, stopwatch.ElapsedMilliseconds);
}
public static void SingleLineTest()
{
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
int count = 0;
for (uint i = 0; i < 1000000000; ++i) {
count += i % 16 == 0 ? 1 : 0;
}
stopwatch.Stop();
Console.WriteLine("Single-line test --> Count: {0}, Time: {1}", count, stopwatch.ElapsedMilliseconds);
}
public static void MultiLineTest()
{
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
int count = 0;
for (uint i = 0; i < 1000000000; ++i) {
var isMultipleOf16 = i % 16 == 0;
count += isMultipleOf16 ? 1 : 0;
}
stopwatch.Stop();
Console.WriteLine("Multi-line test --> Count: {0}, Time: {1}", count, stopwatch.ElapsedMilliseconds);
}
}
Ответ 5
Я обычно думаю об этом так: люди, которые работают над компилятором, могут делать столько всего в год. Если бы за это время они могли реализовать лямбды или множество классических оптимизаций, я бы проголосовал за лямбда. С# - это язык, который эффективен с точки зрения чтения и записи кода, а не с точки зрения времени выполнения.
Поэтому разумно для команды сосредоточиться на функциях, которые максимизируют эффективность чтения/записи, а не эффективности исполнения в определенном случае с углом (из которых, вероятно, тысячи).
Первоначально, я полагаю, идея заключалась в том, что JITTER выполнит всю оптимизацию. К сожалению, JITting занимает заметное количество времени, и любая передовая оптимизация сделает его еще хуже. Так что это не сработало, как можно было надеяться.
Одна вещь, которую я нашел о программировании действительно быстрого кода на С#, заключается в том, что довольно часто вы сталкиваетесь с серьезным узким местом GC до того, как любая оптимизация, как вы упомянули, изменит ситуацию. Например, если вы выделяете миллионы объектов. С# оставляет вам очень мало шансов избежать затрат: вместо этого вы можете использовать массивы структур, но полученный код действительно уродлив в сравнении. Моя точка зрения заключается в том, что многие другие решения о С# и .NET делают такие конкретные оптимизации менее полезными, чем они были бы в компиляторе С++. Heck, они даже снизили оптимизацию для конкретного процессора в NGEN, эффективность торговли для эффективности программы (отладчика).
Сказав все это, мне бы понравился С#, который фактически использовал оптимизации, которые С++ использовал с 1990-х годов. Только не за счет таких функций, как, скажем, async/await.