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