Беззнаковое 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,

Другие вопросы по тегам