Беззнаковое 64-битное в двойное преобразование: почему этот алгоритм из g++
Использование g++ 4.9.2, если я скомпилирую
bool int_dbl_com(const unsigned long long x, const double y)
{
return x <= y;
}
тогда вывод ассемблера (для соглашения о вызовах Windows x64):
testq %rcx, %rcx # x in RCX
js .L2
pxor %xmm0, %xmm0
cvtsi2sdq %rcx, %xmm0
ucomisd %xmm0, %xmm1 # y in 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
, конвертировать в двойной в %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-разрядного в 53-разрядное значение IEEE 754 binary64. Если эти два преобразования реализованы наивно, то в некоторых случаях результат может отличаться от прямого одиночного преобразования 64-разрядного целого числа с плавающей запятой с 53-разрядным значением. Это явление называется "двойным округлением".
Округление до нечетного гарантирует, что результатом промежуточного округления не будет значение, которое будет округлено в неправильном направлении в случае двойного округления. Этого достаточно, чтобы сделать последовательности ниже эквивалентными для всех входных данных:
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
и затем возвращает это к результату. Зачем?? И зачем беспокоиться, если эти младшие биты все равно будут потеряны (или я здесь ошибаюсь)?
Это именно то, как реализовать округление к нечетному в данном конкретном случае. Наименее значимый бит %rcx
равен единице, если сдвиг не является точным делением на два, и в этом случае результат должен быть нечетным.
Один и тот же алгоритм, кажется, используется независимо от оптимизации; Я использовал здесь -O3, чтобы было легче увидеть.
Последовательность команд является оптимальной (насколько я вижу, для современных процессоров) и соответствует преобразованию уровня источника из uint64_t
int to double
, Компилятору не требуется никаких усилий, чтобы использовать его даже на самом низком уровне оптимизации. Что может случиться с оптимизацией (но не происходит здесь), так это то, что инструкции объединены с другими инструкциями, которые соответствуют другим конструкциям исходного уровня. Но нет смысла иметь последовательность команд, отличную от оптимальной для генерации для преобразования в -O0
,