Почему .NET Native компилирует цикл в обратном порядке?

Я работаю над методами оптимизации, выполняемыми компилятором .NET Native. Я создал образец цикла:

        for (int i = 0; i < 100; i++)
        {
            Function();
        }

И я скомпилировал его с помощью Native. Затем я дизассемблировал полученный .dll файл с машинным кодом внутри в IDA. В итоге имею:

Выход IDA

(Я удалил несколько ненужных строк, так что не беспокойтесь, что адресные строки несовместимы)

Я понимаю, что add esi, 0FFFFFFFFh на самом деле означает subtract one from esi and alter Zero Flag if needed, поэтому мы можем перейти к началу, если ноль еще не достигнут.

Чего я не понимаю, так это почему компилятор перевернул цикл?

Я пришел к выводу, что

LOOP:
add esi, 0FFFFFFFFh
jnz LOOP

просто быстрее, чем например

LOOP:
inc esi
cmp esi, 064h
jl LOOP

Но так ли это из-за этого и действительно ли разница в скорости значительна?


person Kamil T    schedule 05.04.2016    source источник
comment
ADD с немедленным значением быстрее, чем INC, и вы также пропускаете CMP... все это в 3 строках кода. Тогда да, разница РЕАЛЬНО значительная (и по размеру, и по скорости). Представьте, что вы делаете это примерно в 30000 мест в реальной программе...   -  person Adriano Repetti    schedule 05.04.2016
comment
Да, это быстрее, и, как правило, оптимизаторы применяют любую возможную оптимизацию, которая делает ваш код быстрее, не изменяя семантику вашей программы.   -  person Ross Ridge    schedule 05.04.2016
comment
Что касается инвертированного направления, возможно, сравнение с нулем происходит быстрее, чем сравнение с конкретным значением?   -  person user5226582    schedule 05.04.2016
comment
Да потому что даже сравнивать не надо. Как вы (не) видите :)   -  person Jester    schedule 05.04.2016
comment
Вы написали код в обоих направлениях. Если вы хотите узнать, быстрее ли один способ, чем другой, запустите их.   -  person Eric Lippert    schedule 05.04.2016
comment
@EricLippert Я не ленивый, и я бы с удовольствием, но сейчас я на своем рабочем ПК, и у меня нет никаких инструментов для запуска или тестирования кода сборки :( Также у меня нет прав администратора, чтобы что-либо устанавливать .   -  person Kamil T    schedule 05.04.2016
comment
Цикл for(;;) обычно требует также проверки того, что начальное значение соответствует конечному условию. Типичный codegen — это ветвь вперед к коду условия, а затем ветвь назад к коду тела цикла. Но здесь оптимизатор может срезать путь, он знает, что начальное значение уже хорошее и что вы нигде не используете значение i, поэтому он может генерировать меньше кода.   -  person Hans Passant    schedule 05.04.2016


Ответы (2)


inc может быть медленнее, чем add из-за частичного обновления флага. Кроме того, add влияет на нулевой флаг, поэтому вам не нужно использовать другую инструкцию cmp. Просто прыгайте прямо.

Это один из известных типов оптимизации цикла.

реверс. Цикл реверсирования меняет порядок, в котором значения присваиваются индексной переменной. Это тонкая оптимизация, которая может помочь устранить зависимости и, таким образом, включить другие оптимизации. Кроме того, в некоторых архитектурах используются циклические конструкции на уровне языка ассемблера, которые учитываются только в одном направлении (например, уменьшение-переход-если-не-ноль (DJNZ)).

Вы можете увидеть результат для других компиляторов здесь.

person phuclv    schedule 05.04.2016
comment
inc медленнее, чем add на один такт. Сравните их в Справочное руководство по оптимизации архитектур Intel® 64 и IA-32. Прокрутите вниз до Приложения C, и вы увидите время задержки и пропускную способность каждой инструкции x86/x64. 1 тактовый цикл может показаться незначительным, но если у вас есть сотни или тысячи циклов, он быстро складывается. - person Icemanind; 05.04.2016
comment
@Icemanind Эти цифры не отражают реальность микроархитектур, которые они описывают (IvyBridge через Skylake; см. Таблицу ранее в этом приложении). Цикл dec/jnz может выполняться с одной итерацией за цикл, а цикл inc/dec имеет задержку только в 1 цикл для целочисленного регистра как часть других цепочек отложений. Возможно, Intel получила 2-тактную задержку на IvyBridge через Broadwell (но не Skylake) из-за просмотра задержки для чтения EFLAGS, возможно, включая CF, для которого потребуется слияние флагов. Но это не проблема для dec/jnz даже без фьюжн или dec / setz. У меня есть только Skylake, поэтому я не могу проверить :/ - person Peter Cordes; 18.10.2019
comment
@Icemanind: также обратите внимание, что это были числа latency; в таблице, на которую вы ссылаетесь, по-прежнему указана пропускная способность увеличения/уменьшения при 0.25 циклах, то есть 4 за такт. Во всяком случае, в таблицах инструкций Агнера Фога, основанных на экспериментальном тестировании, указано увеличение/уменьшение при задержке 1c/пропускной способности 0,25c. Как и uops.info/table.html. uops.info даже измерил задержку от ввода до целочисленного вывода и до вывода флага и обнаружил 1 цикл в обоих случаях: uops.info/html-instr/INC_R32.html. (Не включая CF без выхода) - person Peter Cordes; 18.10.2019
comment
@Icemanind: избегать dec здесь полезно только в том случае, если код может работать на Silvermont или Pentium 4. В противном случае это пустая трата размера кода для основных Intel и AMD. - person Peter Cordes; 18.10.2019

Ваш вывод верен: инвертированный цикл будет нацелен на 0 (цикл завершится, когда значение регистра достигнет 0), так что Add установит нулевой флаг, используемый в условном переходе.

Таким образом, вам не нужен выделенный Cmp, что приводит к: 1) оптимизации размера 2) также быстрее (вывод из решения программистов компилятора и другого ответ).

Это довольно распространенный трюк ассемблера для написания цикла, нацеленного на 0. Я удивлен, что вы понимаете ассемблер, но не знаете (спрашиваете) об этом.

person Sinatr    schedule 05.04.2016