Почему добавление локальных переменных замедляет работу кода .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 project, полезно для проверки настроек проекта.]

Устранение промежуточного логического значения - это фундаментальная оптимизация, одна из самых простых в моей 1980-х гг. Dragon Забронируйте. Как оптимизация не применялась при генерации CIL или JIT машинного кода 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] 

person Edward Brey    schedule 29.04.2012    source источник
comment
Было бы любопытно увидеть другой ассемблерный код. Не могли бы вы опубликовать это?   -  person phoog    schedule 29.04.2012
comment
Вы тестировали bool isMultipleOf16 = ...?   -  person David.Chu.ca    schedule 29.04.2012
comment
@ David.Chu.ca - это не имеет значения - var - это компилятор, пожалуйста, выведите тип этой переменной и представьте, что это написал я. В этом случае для себя будет выведено bool.   -  person Damien_The_Unbeliever    schedule 29.04.2012
comment
как насчет статического isMulitppleOF16?   -  person David.Chu.ca    schedule 29.04.2012
comment
@EdwardBrey: Поскольку вы сделали это в режиме Debug, все ставки отключены   -  person BrokenGlass    schedule 29.04.2012
comment
Единственный способ получить этот код - отключить оптимизатор джиттера. Неверный тест.   -  person Hans Passant    schedule 29.04.2012
comment
@BrokenGlass Вы говорите довольно уверенно. Я был осторожен, чтобы убедиться, что я нахожусь в режиме выпуска и что оптимизация остается включенной. Чтобы убедиться, я отключил оптимизацию и убедился, что сроки стали медленнее, а код сборки увеличился. Я изменил конфигурацию проекта на «Отладку» и включил параметр «Подавить оптимизацию JIT в модуле». Когда я вернулся к оптимизированной версии без подавления, код снова стал меньше и быстрее. Если я делаю ошибку, что мне следует принять к сведению, чтобы ее обнаружить?   -  person Edward Brey    schedule 29.04.2012
comment
@Hans Я пробовал два разных компьютера с одинаковым результатом. Я также повторно реализовал несколько проектов, потому что не мог поверить, что это правда. Тем не менее, результат тот же. Я ничего не сделал, чтобы отключить оптимизаторы JIT в своих системах, хотя не знаю, где это искать. Вы можете репро?   -  person Edward Brey    schedule 29.04.2012
comment
Конечно, я могу получить тот же код, который вы разместили, включив параметр Отладчик оптимизации «Подавить JIT». Оптимизированный код очень отличается.   -  person Hans Passant    schedule 29.04.2012
comment
@Hans В приведенных выше 13 строках ассемблерного кода отключена функция подавления JIT-оптимизации. Если я включу подавление, ситуация станет еще хуже и вырастет до 21 строки ассемблерного кода.   -  person Edward Brey    schedule 29.04.2012
comment
Я не могу воспроизвести ваш результат с теми же версиями ОС и VS + .NET, не знаю, почему вы получаете неоптимизированный код.   -  person Hans Passant    schedule 29.04.2012
comment
@Hans Давайте попробуем сузить круг вопросов. Я поместил репродукцию на GitHub. Что вы получаете за результат?   -  person Edward Brey    schedule 29.04.2012
comment
@EdwardBrey: Я не могу найти источник в данный момент, но я считаю, что дрожание и / или другие настройки оптимизатора отличаются, если у вас есть отладчик вообще (то есть, если вы работает из Visual Studio, даже если вы скомпилировали в режиме выпуска). Попробуйте запустить свой код из командной строки (не из VS) и посмотрите, что произойдет.   -  person Daniel Pryden    schedule 30.04.2012
comment
@Daniel Я запускал все свои временные тесты из командной строки вне Visual Studio. Я запускал только Visual Studio, чтобы получить листинги кода сборки. В качестве дополнительной проверки, только сейчас я попытался запустить тест синхронизации с еще не запущенной Visual Studio. Я все еще вижу разные тайминги.   -  person Edward Brey    schedule 30.04.2012
comment
@EdwardBrey: Запуск кода как x86 заставляет оба запускаться одновременно. Изменить: но только на .NET 4. .NET 2 отличается от обоих.   -  person leppie    schedule 30.04.2012
comment
@leppie Не могли бы вы попробовать конфигурацию x86 в тестовом проекте на github? (В Github есть кнопка ZIP для быстрого получения zip-файла проекта.) Я получаю очень разные тайминги между тестами с одним и двумя операторами, даже для x86.   -  person Edward Brey    schedule 30.04.2012
comment
@EdwardBrey: Я так и сделал.   -  person leppie    schedule 30.04.2012
comment
@leppie Если вы отключите параметр Подавить JIT-оптимизацию в параметрах VS2010 и поставите точку останова на оператор в цикле, вы увидите другой ассемблерный код для разных форм, в частности, добавление инструкций sete, movzx и test для двух- форма заявления?   -  person Edward Brey    schedule 30.04.2012
comment
Когда я сбрасываю JIT-скомпилированный код для двух версий (через WinDbg), я вижу различия, но я также вижу значительно больше кода, чем вы указываете в своем вопросе. Помните, что между строками исходного кода и JIT-скомпилированным кодом не существует прямого сопоставления, поэтому вам следует перечислить все это. Сравнение только тела цикла может не дать достаточно подробностей для сравнения.   -  person Brian Rasmussen    schedule 02.05.2012


Ответы (5)


Вопрос должен быть «Почему я вижу такую ​​разницу на своей машине?». Я не могу воспроизвести такую ​​огромную разницу в скорости и подозреваю, что есть что-то специфическое для вашей среды. Хотя очень сложно сказать, что это может быть. Это могут быть некоторые параметры (компилятора), которые вы установили некоторое время назад и забыли о них.

Я создал консольное приложение, перестроил его в режиме выпуска (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% - все еще много. Кажется, есть ошибка в 64-битном JIT-компиляторе .NET4.

person Maciej    schedule 30.04.2012
comment
Первый метод: 1.8736291s, второй метод: 1.8566318s на моей машине, перестроенный с Release (x86), работал вне VS, используя тот же самый код. - person ShdNx; 30.04.2012
comment
Вам нужно что-то сделать с count (например, включить его в свой WriteLine оператор). В противном случае оптимизатор выполняет некоторую выборочную оптимизацию, которая меняется со временем. - person Edward Brey; 30.04.2012
comment
@EdwardBrey, я изменил свои 64-битные тесты и получил те же результаты, что и вы. Подправил свой ответ, чтобы отразить. - person Maciej; 30.04.2012
comment
@EdwardBrey, я могу воспроизвести это только в 64-битном приложении - person Maciej; 30.04.2012
comment
Я получил 1,86 с (1-й) и 1,38 с (2-й) в x64, в то время как в x86 у меня было около 2,09 с для обоих методов. - person Ral Zarek; 30.04.2012
comment
Я обновил тестовый проект на github, включив в него конфигурации x64 и x86. После выполнения пакетной перестройки обеих конфигураций и запуска каждого .exe за пределами VS2010 на двух компьютерах (оба с Core i5) я вижу очень разные тайминги для Test1 и Test2 как на x64, так и на x86. Мацей и Маркус, чтобы помочь устранить переменные, не могли бы вы взять проект и попробовать? (На Github есть кнопка ZIP, чтобы быстро получить zip-файл проекта.) - person Edward Brey; 30.04.2012
comment
@EdwardBrey Запуск вашего теста дает мне более медленное выполнение в многострочной версии на обеих платформах. Но если я изменю тест так, чтобы он запускал 4xMultiline, а затем 4xSingleline, разницы в скорости на x86 не будет (x64 не затронут) - person Maciej; 01.05.2012
comment
@Maciej +1 за отличное наблюдение. Похоже, что важно, какой метод вызывается первым. Похоже, что это почти наверняка связано с порядком JIT. Но почему так важен порядок JIT, вызывает недоумение. Я обновил текст вопроса ссылкой на новый вопрос, вызванный вашим наблюдением . - person Edward Brey; 02.05.2012
comment
@EdwardBrey Можете ли вы проголосовать за мой ответ вместо комментария, так как он покажет настоящую признательность :-) - person Maciej; 02.05.2012
comment
@Maciej Извините, но я не думаю, что ваш ответ действительно отвечает на вопрос. Это помогло нам найти сбивающую с толку проблему, а именно то, что для x86 штрафы за выравнивание могут привести к тому, что хорошо оптимизированный код будет работать так же медленно, как и неполностью оптимизированный код. Но все же остается вопрос: почему бывают случаи, когда в первую очередь генерируется не полностью оптимизированный код для x64 (и x86)? - person Edward Brey; 02.05.2012

Я не могу говорить ни с компилятором .NET, ни с его оптимизациями, ни даже КОГДА он выполняет свои оптимизации.

Но в этом конкретном случае, если компилятор свернет эту логическую переменную в фактический оператор, и вы попытаетесь отладить этот код, оптимизированный код не будет соответствовать написанному коду. Вы не сможете пройти через назначение isMulitpleOf16 и проверить его значение.

Это всего лишь один из примеров того, как оптимизация вполне может быть отключена. Могли быть и другие. Оптимизация может происходить на этапе загрузки кода, а не на этапе генерации кода из среды CLR.

Современные среды выполнения довольно сложны, особенно если вы добавляете JIT и динамическую оптимизацию во время выполнения. Я благодарен, что код иногда делает то, что говорит.

person Will Hartung    schedule 29.04.2012
comment
Я задумался, когда увидел ассемблерный код, не отключена ли каким-то образом оптимизация. Я получил код сборки, остановившись на точке останова в отладчике VS2010 и используя окно «Разборка» (тогда как время, полученное при запуске без отладчика). В качестве теста я включил Инструменты ›Параметры› Отладка ›Общие› Подавить оптимизацию JIT при настройке модуля. Конечно, ассемблерный код стал еще больше. - person Edward Brey; 29.04.2012
comment
В родном мире C ++ совершенно нормально, что точки останова и порядок кода могут быть странными, когда включена оптимизация. Точно так же переменные типа isMultipleOf16 не всегда доступны в отладчике. Поэтому и есть режим отладки. В конце концов, мы по-прежнему работаем с тем же машинным кодом, поэтому я не понимаю, почему CLR может что-то изменить. Действительно, когда в C # возникают исключения, даже в режиме отладки я иногда получаю сообщение о том, что значение переменной оптимизируется, даже в режиме отладки. - person Edward Brey; 29.04.2012
comment
+1 за замечание, что настройки отладки могут повлиять на генерацию кода. - person Marco van de Voort; 29.04.2012

Это ошибка .NET Framework.

На самом деле я просто размышляю, но я отправил отчет об ошибке на Microsoft Connect, чтобы узнать, что они говорят. После того, как Microsoft удалила этот отчет, я повторно отправил его в проект roslyn на GitHub.

Обновление: Microsoft перенесла проблему в проект coreclr. Судя по комментариям к проблеме, называть это ошибкой кажется слишком сильным; это скорее недостающая оптимизация.

person Edward Brey    schedule 30.04.2012
comment
Если бы у меня был доллар за каждый раз, когда мне говорили программисты, мой код не работал бы. Это должно быть ошибка во фреймворке (или компиляторе, или библиотеке времени выполнения и т. Д.), И, как позже выяснилось, что это ошибка в его собственном коде, я мог бы удалиться. - person Jim Mischel; 02.05.2012
comment
@ Джим: Я сам видел это много раз. Лучшее противоядие, которое я знаю, - максимально фундаментально изолировать поведение и предоставить поставщику репродукцию. И сохранять выжидательную позицию. Вот где мы находимся. - person Edward Brey; 07.05.2012
comment
@TankorSmash Возможно, Microsoft удалила его как часть переноса кода на GitHub. Его больше нет на моей панели управления Connect. Кажется, что многие проблемы, о которых я сообщал, исчезли. Было бы неплохо получить какое-то уведомление. Я повторно отправил проблему в проект GitHub и соответственно обновил ответ. - person Edward Brey; 04.11.2015

Я думаю, это связано с вашим другим вопросом. Когда я изменяю ваш код следующим образом, многострочная версия выигрывает.

ой, только на 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);
    }
}
person Ben Voigt    schedule 02.05.2012
comment
Я обновил проект воспроизведения, включив в него тест if. Я делал одно- и многострочные варианты. И на x64, и на x86 однострочная версия работает быстрее (когда нет штрафа за выравнивание). Я также сделал варианты, в которых вообще нет условного кода в цикле (просто битовая математика). На x86 они горло (сборку не проверял). На x64 версия с локальной переменной работает быстрее! По-прежнему удивительно, что локальная переменная вообще имеет значение. - person Edward Brey; 03.05.2012

Я склонен думать об этом так: люди, которые работают над компилятором, могут делать только определенное количество вещей в год. Если бы за это время они могли реализовать лямбды или множество классических оптимизаций, я бы проголосовал за лямбды. C # - это язык, который эффективен с точки зрения чтения и записи кода, а не с точки зрения времени выполнения.

Поэтому для команды разумно сконцентрироваться на функциях, которые максимизируют эффективность чтения / записи, а не на эффективности выполнения в определенных угловых случаях (которых, вероятно, тысячи).

Я считаю, что изначально идея заключалась в том, что всю оптимизацию будет выполнять JITter. К сожалению, JITting занимает заметное количество времени, и любая продвинутая оптимизация только усугубит ситуацию. Так что это не сработало так хорошо, как можно было надеяться.

Одна вещь, которую я обнаружил в программировании действительно быстрого кода на C #, заключается в том, что довольно часто вы сталкиваетесь с серьезным узким местом GC, прежде чем любая оптимизация, о которой вы упомянули, будет иметь значение. Как если бы вы разместили миллионы объектов. C # оставляет вам очень мало с точки зрения избежания затрат: вместо этого вы можете использовать массивы структур, но результирующий код действительно уродлив по сравнению. Я хочу сказать, что многие другие решения, касающиеся C # и .NET, делают такие конкретные оптимизации менее целесообразными, чем они были бы в чем-то вроде компилятора C ++. Черт возьми, они даже отказались от оптимизации для конкретного процессора в NGEN, торговля производительностью для эффективности программиста (отладчика).

Сказав все это, я бы полюбил C #, который фактически использовал оптимизацию, которую C ++ использовал с 1990-х годов. Только не за счет таких функций, как, скажем, async / await.

person Roman Starkov    schedule 06.05.2012
comment
Я бы очень опасался зачитывать слишком много статей из 2005 и .net 1.1! За последние 7 лет многое изменилось. - person Jason Williams; 06.05.2012