Здесь мой код:
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, что и для исходной версии, так что обещание.