Ответ 1
Эта оптимизация не только возможна, она теперь доступна в gcc-6: https://gcc.gnu.org/viewcvs/gcc?view=revision&revision=222077
Здесь мой код:
int f(double x, double y)
{
return std::isnan(x) || std::isnan(y);
}
Если вы используете C вместо С++, просто замените std::
на __builtin_
(не просто удалите std::
по причинам, указанным здесь: Почему GCC реализовать isnan() более эффективно для С++ <cmath> чем C < math.h > ?).
Здесь сборка:
ucomisd %xmm0, %xmm0 ; set parity flag if x is NAN
setp %dl ; copy parity flag to %edx
ucomisd %xmm1, %xmm1 ; set parity flag if y is NAN
setp %al ; copy parity flag to %eax
orl %edx, %eax ; OR one byte of each result into a full-width register
Теперь попробуйте альтернативную формулировку, которая делает то же самое:
int f(double x, double y)
{
return std::isunordered(x, y);
}
Здесь сборка для альтернативы:
xorl %eax, %eax
ucomisd %xmm1, %xmm0
setp %al
Это здорово - мы сокращаем сгенерированный код почти вдвое! Это работает, потому что ucomisd
устанавливает флаг четности, если один из его операндов является NAN, поэтому мы можем тестировать два значения за раз, SIMD-стиль.
Вы можете видеть код, например, оригинальную версию в дикой природе, например: https://svn.r-project.org/R/trunk/src/nmath/qnorm.c
Если бы мы могли сделать GCC достаточно умным, чтобы объединить два вызова isnan()
во всем мире, это было бы довольно круто. Мой вопрос: можем ли мы и как? У меня есть некоторое представление о том, как работают компиляторы, но я не знаю, где в GCC такая оптимизация может быть выполнена. Основная идея - всякий раз, когда есть пара вызовов isnan()
(или __builtin_isnan
) OR'd вместе, она должна издавать одну команду ucomisd
, используя два операнда одновременно.
Отредактировано, чтобы добавить некоторые исследования, вызванные Базиле Старинкевичем:
Если я скомпилирую с -fdump-tree-all, я нахожу два файла, которые кажутся релевантными. Во-первых, *.gimple
содержит это (и немного больше):
D.2229 = x unord x;
D.2230 = y unord y;
D.2231 = D.2229 | D.2230;
Здесь мы можем ясно видеть, что GCC знает, что он пройдет (x, x)
до isunordered()
. Если мы хотим оптимизировать преобразование на этом уровне, это правило будет примерно следующим: "Заменить a unord a | b unord b
на a unord b
". Это то, что вы получаете при компиляции моего второго кода C:
D.2229 = x unord y;
Другим интересным файлом является *.original
:
return <retval> = (int) (x unord x || y unord y);
Это фактически весь файл без комментария, созданный -fdump-tree-original
. И для лучшего исходного кода это выглядит так:
return <retval> = x unord y;
Очевидно, что такое же преобразование можно применить (просто здесь ||
вместо |
).
Но, к сожалению, если мы изменим исходный код, например:
if (__builtin_isnan(x))
return true;
if (__builtin_isnan(y))
return true;
return false;
Затем мы получаем совершенно разные выходные файлы Gimple и Original, хотя последняя сборка такая же, как и раньше. Так, может быть, лучше попробовать эту трансформацию на более позднем этапе в конвейере? Файл *.optimized
(среди прочих) показывает тот же код для версии с "if" s, что и для исходной версии, так что обещание.
Эта оптимизация не только возможна, она теперь доступна в gcc-6: https://gcc.gnu.org/viewcvs/gcc?view=revision&revision=222077
Есть два вопроса:
- это оптимизация, которую вы предлагаете всегда законной в строгом стандарте С++ 11 (я не знаю).
можно настроить GCC, добавив такую оптимизацию: да! Вы можете расширить его, используя MELT -e.g. напишите свое собственное расширение MELT, сделав это, или с вашим собственным плагином GCC, закодированным (больно) на С++.
Однако добавление дополнительной оптимизации в GCC - это значительная работа (даже с MELT): вам нужно понять внутренности GCC. Таким образом, это больше, чем неделю работы.
И я не уверен, что такая оптимизация действительно стоит усилий.