Суровые различия в сборке сборок с плавающей запятой <и> =

Я экспериментирую с созданной сборкой и нашел интересную вещь. Есть две функции, выполняющие одинаковые вычисления. Единственное различие между ними - это способ суммирования результатов.

#include <cmath>

double func1(double x, double y)
{
  double result1;
  double result2;

  if (x*x < 0.0) result1 = 0.0;
  else
  {
    result1 = x*x+x+y;
  }

  if (y*y < 0.0) result2 = 0.0;
  else
  {
    result2 = y*y+y+x;
  }

  return (result1 + result2) * 40.0;
}

double func2(double x, double y)
{
  double result = 0.0;

  if (x*x >= 0.0)
  {
    result += x*x+x+y;
  }

  if (y*y >= 0.0)
  {
    result += y*y+y+x;
  }

  return result * 40.0;
}

Сборка, сгенерированная с помощью x86 clang 3.7 с -O2 на gcc.godbolt.org, пока еще очень отличается и неожиданна. (компиляция на gcc приводит к аналогичной сборке)

    .LCPI0_0:
    .quad   4630826316843712512     # double 40
func1(double, double):                             # @func1(double, double)
    movapd  %xmm0, %xmm2
    mulsd   %xmm2, %xmm2
    addsd   %xmm0, %xmm2
    addsd   %xmm1, %xmm2
    movapd  %xmm1, %xmm3
    mulsd   %xmm3, %xmm3
    addsd   %xmm1, %xmm3
    addsd   %xmm0, %xmm3
    addsd   %xmm3, %xmm2
    mulsd   .LCPI0_0(%rip), %xmm2
    movapd  %xmm2, %xmm0
    retq

.LCPI1_0:
    .quad   4630826316843712512     # double 40
func2(double, double):                             # @func2(double, double)
    movapd  %xmm0, %xmm2
    movapd  %xmm2, %xmm4
    mulsd   %xmm4, %xmm4
    xorps   %xmm3, %xmm3
    ucomisd %xmm3, %xmm4
    xorpd   %xmm0, %xmm0
    jb  .LBB1_2
    addsd   %xmm2, %xmm4
    addsd   %xmm1, %xmm4
    xorpd   %xmm0, %xmm0
    addsd   %xmm4, %xmm0
.LBB1_2:
    movapd  %xmm1, %xmm4
    mulsd   %xmm4, %xmm4
    ucomisd %xmm3, %xmm4
    jb  .LBB1_4
    addsd   %xmm1, %xmm4
    addsd   %xmm2, %xmm4
    addsd   %xmm4, %xmm0
.LBB1_4:
    mulsd   .LCPI1_0(%rip), %xmm0
    retq

func1 компилируется в ветвящуюся сборку с гораздо меньшими инструкциями, чем func2. таким образом, func2 ожидается намного медленнее, чем func1.

Может ли кто-нибудь объяснить это поведение?

Ответ 1

Причина такого поведения операторов сравнения < или >= отличается от того, является ли ваш double NaN или не NaN. Все сравнения, в которых один из операндов NaN возвращает false. Таким образом, ваш x*x < 0.0 всегда будет ложным независимо от того, является ли x NaN или нет. Таким образом, компилятор может безопасно оптимизировать это. Однако случай x * x >= 0 будет вести себя по-разному для значений NaN и non NaN, поэтому компилятор оставляет условные переходы в сборке.

Это то, что cppreference говорит о сравнении с участием NaN:

значения операндов после преобразования сравниваются в обычном математическом смысле (за исключением того, что положительные и отрицательные нули сравниваются равными, и любое сравнение с использованием значения NaN возвращает ноль)