Unsigned 64-бит для двойного преобразования: почему этот алгоритм из g++

Использование g++ 4.9.2, если я компилирую

bool int_dbl_com(const unsigned long long x, const double y)
{
    return x <= y;
}

тогда выход ассемблера:

testq     %rcx, %rcx
js        .L2
pxor      %xmm0, %xmm0
cvtsi2sdq %rcx, %xmm0
ucomisd   %xmm0, %xmm1
setae     %al
ret

Команда cvtsi2sdq - это преобразование с подписью, а первая комбинация тестов и прыжков - проверить, есть ли %rcx < 0. Если это так, переходим к L2, и этого я не понимаю:

.L2:
movq       %rcx, %rax
andl       $1, %ecx
pxor       %xmm0, %xmm0
shrq       %rax
orq        %rcx, %rax
cvtsi2sdq  %rax, %xmm0
addsd      %xmm0, %xmm0
ucomisd    %xmm0, %xmm1
setae      %al
ret

Наилучшим образом, вы можете вдвое уменьшить %rcx, преобразовать в double в %xmm0, а затем добавить %xmm0 в себя, чтобы вернуть исходное значение (признав, конечно, что вы потеряли некоторую точность низкого порядка переход от 64-битного целого к 64-битовому поплавку).

Но это не то, что делает код: он, кажется, сохраняет бит младшего разряда %rcx, а затем возвращает его обратно в результат. Зачем?? И зачем беспокоиться, когда эти бит младшего порядка будут потеряны в любом случае (или я ошибаюсь здесь)?

(Тот же алгоритм, по-видимому, используется независимо от оптимизации, я использовал -O3 здесь, чтобы было легче видеть.)

Ответы

Ответ 1

.L2:
movq       %rcx, %rax
andl       $1, %ecx       ; save the least significant bit of %rax
pxor       %xmm0, %xmm0
shrq       %rax           ; make %rax represent half the original number, as a signed value
orq        %rcx, %rax     ; "round to odd": if the division by two above was not exact, ensure the result is odd
cvtsi2sdq  %rax, %xmm0    ; convert to floating-point
addsd      %xmm0, %xmm0   ; multiply by two
ucomisd    %xmm0, %xmm1   ; compare …
setae      %al
ret

Последние три команды реализуют <= и return из исходного кода. Остальные являются частью преобразования от uint64_t до double.

Трудный для понимания шаг - это тот, который я прокомментировал как "круглый до нечетного". "Округление до нечетного" - это метод, который предотвращает неприятные эффекты "двойное округление" .

Фактически, алгоритм должен преобразовывать с 64-битного в 63-битный, а затем из 63 бит в бинарный код IEEE 754. Если реализовать наивно, в некоторых случаях эти два преобразования могут привести к результату, который отличается от прямого однократного преобразования от 64-разрядного целого к плавающей точке. Это то, что называется "двойное округление".

Округление до нечетного гарантирует, что результат промежуточного округления не будет значением, которое будет округлено в неправильном направлении в случае двойное округление. Этого достаточно, чтобы последовательности, эквивалентные для всех входов:

64-bit ---(round to odd)---> 63-bit ---(round to nearest even)----> binary64 
64-bit -(round-to-nearest-even,the conversion the compiler wants)-> binary64

Чтобы ответить на другие аспекты вашего вопроса:

Но это не то, что делает код: похоже, он сохраняет бит младшего разряда %rcx, а затем возвращает его обратно в результат. Зачем?? И зачем беспокоиться, когда эти бит младшего порядка будут потеряны в любом случае (или я ошибаюсь здесь)?

Это точно, как реализовать в этом конкретном экземпляре round-to-odd. Наименьший значащий бит %rcx равен единице, если сдвиг не является точным делением на два, и в этом случае результат должен быть выполнен нечетным.

Тот же алгоритм, по-видимому, используется независимо от оптимизации; Я использовал -O3 здесь, чтобы было легче видеть.

Последовательность команд оптимальна (насколько я могу видеть, для современных процессоров) и соответствует преобразованию исходного уровня из uint64_t int в double. Это не требует усилий от компилятора, чтобы использовать его даже на самом низком уровне оптимизации. Что может произойти с оптимизацией (но этого не происходит здесь), так это то, что инструкции объединены с другими инструкциями, которые соответствуют другим конструкциям исходного уровня. Но нет смысла иметь другую последовательность команд, чем оптимальную для генерации для преобразований в -O0.