При написании оптимизированной ftol
функции я обнаружил очень странное поведение в GCC 4.6.1
. Позвольте мне сначала показать вам код (для ясности я отметил различия):
fast_trunc_one, C:
int fast_trunc_one(int i) {
int mantissa, exponent, sign, r;
mantissa = (i & 0x07fffff) | 0x800000;
exponent = 150 - ((i >> 23) & 0xff);
sign = i & 0x80000000;
if (exponent < 0) {
r = mantissa << -exponent; /* diff */
} else {
r = mantissa >> exponent; /* diff */
}
return (r ^ -sign) + sign; /* diff */
}
fast_trunc_two, C:
int fast_trunc_two(int i) {
int mantissa, exponent, sign, r;
mantissa = (i & 0x07fffff) | 0x800000;
exponent = 150 - ((i >> 23) & 0xff);
sign = i & 0x80000000;
if (exponent < 0) {
r = (mantissa << -exponent) ^ -sign; /* diff */
} else {
r = (mantissa >> exponent) ^ -sign; /* diff */
}
return r + sign; /* diff */
}
Кажется, так же верно? Ну, GCC не согласен. После компиляции gcc -O3 -S -Wall -o test.s test.c
это вывод сборки:
fast_trunc_one, сгенерированный:
_fast_trunc_one:
LFB0:
.cfi_startproc
movl 4(%esp), %eax
movl $150, %ecx
movl %eax, %edx
andl $8388607, %edx
sarl $23, %eax
orl $8388608, %edx
andl $255, %eax
subl %eax, %ecx
movl %edx, %eax
sarl %cl, %eax
testl %ecx, %ecx
js L5
rep
ret
.p2align 4,,7
L5:
negl %ecx
movl %edx, %eax
sall %cl, %eax
ret
.cfi_endproc
fast_trunc_two, сгенерировано:
_fast_trunc_two:
LFB1:
.cfi_startproc
pushl %ebx
.cfi_def_cfa_offset 8
.cfi_offset 3, -8
movl 8(%esp), %eax
movl $150, %ecx
movl %eax, %ebx
movl %eax, %edx
sarl $23, %ebx
andl $8388607, %edx
andl $255, %ebx
orl $8388608, %edx
andl $-2147483648, %eax
subl %ebx, %ecx
js L9
sarl %cl, %edx
movl %eax, %ecx
negl %ecx
xorl %ecx, %edx
addl %edx, %eax
popl %ebx
.cfi_remember_state
.cfi_def_cfa_offset 4
.cfi_restore 3
ret
.p2align 4,,7
L9:
.cfi_restore_state
negl %ecx
sall %cl, %edx
movl %eax, %ecx
negl %ecx
xorl %ecx, %edx
addl %edx, %eax
popl %ebx
.cfi_restore 3
.cfi_def_cfa_offset 4
ret
.cfi_endproc
Это крайняя разница. На самом деле это также отображается в профиле, fast_trunc_one
примерно на 30% быстрее, чем fast_trunc_two
. Теперь мой вопрос: что вызывает это?
-S -O3 -da -fdump-tree-all
. Это создаст много снимков промежуточного представления. Пройдите по ним (они пронумерованы) бок о бок, и вы сможете найти недостающую оптимизацию в первом случае.int
наunsigned int
и посмотрите, исчезнет ли разница.(r + shifted) ^ sign
не совпадает сr + (shifted ^ sign)
. Я думаю, что это сбивает с толку оптимизатор? FWIW, MSVC 2010 (16.00.40219.01) создает списки, которые практически идентичны друг другу: gist.github.com/2430454Ответы:
Обновлен для синхронизации с редактированием OP
Работая с кодом, мне удалось увидеть, как GCC оптимизирует первый случай.
Прежде чем мы сможем понять, почему они такие разные, сначала мы должны понять, как оптимизируется GCC
fast_trunc_one()
.Верьте или нет,
fast_trunc_one()
оптимизируется для этого:Это производит точно такую же сборку, как оригинал
fast_trunc_one()
- имена регистров и все.Обратите внимание, что
xor
в сборке нет sfast_trunc_one()
. Это то, что отдали мне.Как так?
Шаг 1:
sign = -sign
Во-первых, давайте посмотрим на
sign
переменную. Посколькуsign = i & 0x80000000;
существует только два возможных значенияsign
:sign = 0
sign = 0x80000000
Теперь признайте, что в обоих случаях
sign == -sign
. Поэтому, когда я изменяю исходный код на это:Он производит ту же сборку, что и оригинал
fast_trunc_one()
. Я избавлю вас от сборки, но она идентична - зарегистрируйте имена и все.Шаг 2: Математическое сокращение:
x + (y ^ x) = y
sign
может принимать только одно из двух значений0
или0x80000000
.x = 0
, тогдаx + (y ^ x) = y
тогда тривиально.0x80000000
тому же. Он переворачивает знак. Поэтомуx + (y ^ x) = y
также имеет место, когдаx = 0x80000000
.Следовательно,
x + (y ^ x)
сводится кy
. И код упрощает это:Опять же, это компилируется в точно такую же сборку - имена регистров и все.
Данная версия, наконец, сводится к следующему:
что в значительной степени именно то, что GCC генерирует в сборке.
Так почему же компилятор не оптимизирует
fast_trunc_two()
до одного и того же?Ключевой частью
fast_trunc_one()
являетсяx + (y ^ x) = y
оптимизация. Вfast_trunc_two()
вx + (y ^ x)
выражении раскалывается по отрасли.Я подозреваю, что этого может быть достаточно, чтобы запутать GCC, чтобы не проводить эту оптимизацию. (Это должно было бы поднять
^ -sign
из ветви и объединить это вr + sign
конце.)Например, это производит ту же сборку, что и
fast_trunc_one()
:источник
Это природа компиляторов. Предполагать, что они пойдут по самому быстрому или лучшему пути, совершенно неверно. Любой, кто подразумевает, что вам не нужно ничего делать с вашим кодом для оптимизации, потому что «современные компиляторы» заполняют пробелы, делают лучшую работу, делают самый быстрый код и т. Д. На самом деле я видел, что gcc ухудшается с 3.x до 4.x на руке по крайней мере. 4.x к этому моменту мог догнать до 3.x, но на ранних этапах он создавал более медленный код. С практикой вы можете научиться писать свой код, чтобы компилятору не приходилось работать так усердно, и в результате вы получите более согласованные и ожидаемые результаты.
Ошибка здесь - ваши ожидания того, что будет произведено, а не то, что было произведено на самом деле. Если вы хотите, чтобы компилятор генерировал одинаковые выходные данные, передавайте ему те же входные данные. Математически не то же самое, не то же самое, но на самом деле то же самое, никаких разных путей, никаких операций совместного использования или распространения из одной версии в другую. Это хорошее упражнение для понимания того, как писать свой код, и для понимания того, что с ним делают компиляторы. Не делайте ошибку, предполагая, что, поскольку одна версия gcc для одного целевого процессора однажды дала определенный результат, это правило для всех компиляторов и всего кода. Вы должны использовать множество компиляторов и множество целей, чтобы понять, что происходит.
gcc довольно противный, я приглашаю вас заглянуть за кулисы, посмотреть на внутренности gcc, попытаться добавить цель или что-то изменить самостоятельно. Это только скреплено клейкой лентой и проволокой. Дополнительная строка кода добавлена или удалена в критических местах, и она рушится. Тот факт, что он вообще создал пригодный для использования код - это то, чем нужно радоваться, вместо того, чтобы беспокоиться о том, почему он не оправдывает других ожиданий.
Вы смотрели на какие разные версии GCC производят? 3.x и 4.x, в частности, 4.5 против 4.6 против 4.7 и т. Д.? и для разных целевых процессоров, x86, arm, mips и т. д. или разных разновидностей x86, если вы используете нативный компилятор, 32-битный или 64-битный и т. д.? А потом llvm (лязг) для разных целей?
Mystical проделал отличную работу в мыслительном процессе, необходимом для решения проблемы анализа / оптимизации кода, ожидая, что компилятор придумает что-либо из этого, чего, в принципе, не ожидает ни один «современный компилятор».
Не вдаваясь в математические свойства, код этой формы
собирается привести компилятор к A: реализовать его в этой форме, выполнить if-then-else, а затем сходиться к общему коду для завершения и возврата. или B: сохранить ветку, так как это хвостовой конец функции. Также не беспокойтесь об использовании или сохранении r.
Затем вы можете войти, как указал Мистикал, переменная знака полностью исчезнет для написанного кода. Я бы не ожидал, что компилятор увидит, что переменная sign исчезнет, поэтому вы должны были сделать это сами, а не заставлять компилятор пытаться это выяснить.
Это прекрасная возможность покопаться в исходном коде gcc. Похоже, вы нашли случай, когда оптимизатор увидел одну вещь в одном случае, а другую - в другом. Затем сделайте следующий шаг и посмотрите, не можете ли вы заставить gcc увидеть это дело. Каждая оптимизация существует, потому что какой-то человек или группа распознали оптимизацию и намеренно поместили ее там. Чтобы эта оптимизация была там и работала каждый раз, когда кто-то должен был поместить ее туда (а затем протестировать и сохранить в будущем).
Определенно не думайте, что меньше кода быстрее, а больше кода медленнее, очень легко создать и найти примеры того, что это не так. Чаще всего это происходит из-за того, что меньше кода работает быстрее, чем больше кода. Как я продемонстрировал с самого начала, вы можете создать больше кода для сохранения разветвлений в этом случае или циклов и т. Д., И в результате вы получите более быстрый код.
Суть в том, что вы указали компилятору другой источник и ожидали одинаковых результатов. Проблема не в выводе компилятора, а в ожиданиях пользователя. Для конкретного компилятора и процессора довольно легко продемонстрировать добавление одной строки кода, которая значительно замедляет работу всей функции. Например, почему меняется a = b + 2; а = b + с + 2; причина _fill_in_the_blank_compiler_name_ генерировать радикально другой и медленный код? Ответ, конечно, заключается в том, что компилятор вводил другой код на входе, поэтому вполне допустимо, чтобы компилятор генерировал другой вывод. (еще лучше, когда вы меняете две несвязанные строки кода и резко меняете вывод) Нет ожидаемой связи между сложностью и размером ввода и сложностью и размером вывода.
Произведено где-то между 60-100 линиями ассемблера. Это развернуло петлю. Я не считал строки, если вы думаете об этом, он должен добавить, скопировать результат на вход для вызова функции, сделать вызов функции, минимум три операции. таким образом, в зависимости от цели, вероятно, 60 команд, по меньшей мере, 80, если четыре на цикл, 100, если пять на цикл, и т. д.
источник
Mysticial уже дал отличное объяснение, но я решил добавить, FWIW, что в действительности нет ничего фундаментального в том, почему компилятор выполняет оптимизацию для одного, а не для другого.
clang
Например, компилятор LLVM выдает одинаковый код для обеих функций (кроме имени функции), давая:Этот код не такой короткий, как первая версия gcc из OP, но не такой длинный, как вторая.
Код от другого компилятора (который я не буду называть), компилирующий для x86_64, производит это для обеих функций:
что увлекательно тем, что он вычисляет обе стороны
if
и затем использует условное движение в конце, чтобы выбрать правильное.Компилятор Open64 производит следующее:
и похожий, но не идентичный код для
fast_trunc_two
.В любом случае, когда дело доходит до оптимизации, это лотерея - это то, чем она является ... Не всегда легко понять, почему ваш код компилируется каким-то особым образом.
источник
icc
. У меня есть только 32-битный вариант, но он производит код, очень похожий на этот.