Ответ 1
GCC использует совершенно другой синтаксис для встроенной сборки, чем MSVC, поэтому для работы над обеими формами он довольно много работает. Это тоже не очень хорошая идея. Существует множество проблем с встроенной сборкой. Люди часто используют это, потому что думают, что это заставит их код работать быстрее, но обычно это имеет совершенно противоположный эффект. Если вы не специалист по языку ассемблера и стратегии генерации кода компилятора, вам гораздо лучше позволить разработчику компилятора генерировать код.
Когда вы пытаетесь это сделать, вам нужно быть немного осторожным, хотя: подписанные правые сдвиги определены в C, поэтому, если вы заботитесь о переносимости, вам нужно передать значение эквивалентному типу без знака
#include <limits.h> // for CHAR_BIT
signed long ROR13(signed long val)
{
return ((unsigned long)val >> 13) |
((unsigned long)val << ((sizeof(val) * CHAR_BIT) - 13));
}
(См. также Рекомендации по работе с круговым сдвигом (поворот) в С++).
Это будет иметь ту же семантику, что и исходный код: ROR val, 13
. Фактически, MSVC будет генерировать именно этот объектный код, как и GCC. (Интересно, что Clang будет делать ROL val, 19
, который дает тот же результат, учитывая, как работают повороты. ICC 17 вместо этого генерирует расширенный сдвиг: SHLD val, val, 19
. Я не уверен, почему, возможно, что быстрее, чем вращение на определенных Процессоры Intel, или, может быть, то же самое на Intel, но медленнее на AMD.)
Чтобы реализовать Div16
в чистом C, вы хотите:
signed long Div16(signed long a, signed long b)
{
return ((long long)a << 16) / b;
}
В 64-битной архитектуре, которая может выполнять собственное 64-разрядное деление (предполагается, что long
по-прежнему является 32-разрядным типом, как в Windows), это будет преобразовано в:
movsxd rax, a # sign-extend from 32 to 64, if long wasn't already 64-bit
shl rax, 16
cqo # sign-extend rax into rdx:rax
movsxd rcx, b
idiv rcx # or idiv b if the inputs were already 64-bit
ret
К сожалению, на 32-битной x86 код не так хорош. Компиляторы вызывают вызов в свою внутреннюю библиотечную функцию, которая обеспечивает расширенное 64-битное деление, потому что они не могут доказать, что использование одного 64b/32b = > 32b idiv
инструкция не будет виновата. (Это приведет к возникновению исключения #DE
, если фактор не помещается в eax
, а не просто усекает)
Другими словами, преобразование:
int32_t Divide(int64_t a, int32_t b)
{
return (a / b);
}
в
mov eax, a_low
mov edx, a_high
idiv b # will fault if a/b is outside [-2^32, 2^32-1]
ret
не является правовой оптимизацией - компилятор не может испускать этот код. В стандарте языка говорится, что разделение 64/32 продвигается до 64/64 деления, которое всегда дает результат 64 бит. То, что вы позже приводили или принуждали этот 64-разрядный результат к 32-битовому значению, не имеет отношения к семантике самой операции деления. Ошибка для некоторых комбинаций a
и b
будет нарушать правило as-if, если компилятор не сможет доказать, что эти комбинации a
и b
невозможны. (Например, если b
, как известно, больше, чем 1<<16
, это может быть правовой оптимизацией для a = (int32_t)input; a <<= 16;
. Но даже если это приведет к тому же поведению, что и абстрактная машина C для всех входов, gcc и clang
в настоящее время не делают эту оптимизацию.)
Нет простого способа переопределить правила, установленные стандартом языка, и заставить компилятор испускать желаемый объектный код. MSVC не предлагает для этого встроенного (хотя есть функция Windows API, MulDiv
, он не быстрый и просто использует встроенную сборку для собственной реализации — и ошибка в определенном случае, теперь закрепилась благодаря необходимости обратной совместимости). У вас практически нет выбора, кроме как прибегнуть к сборке, встроенной или связанной с внешним модулем.
Итак, вы попадаете в уродство. Это выглядит так:
signed long Div16(signed long a, signed long b)
{
#ifdef __GNUC__ // A GNU-style compiler (e.g., GCC, Clang, etc.)
signed long quotient;
signed long remainder; // (unused, but necessary to signal clobbering)
__asm__("idivl %[divisor]"
: "=a" (quotient),
"=d" (remainder)
: "0" ((unsigned long)a << 16),
"1" (a >> 16),
[divisor] "rm" (b)
:
);
return quotient;
#elif _MSC_VER // A Microsoft-style compiler (i.e., MSVC)
__asm
{
mov eax, DWORD PTR [a]
mov edx, eax
shl eax, 16
sar edx, 16
idiv DWORD PTR [b]
// leave result in EAX, where it will be returned
}
#else
#error "Unsupported compiler"
#endif
}
Это приводит к желаемому результату как для компиляторов Microsoft, так и для GNU.
Ну, в основном. По какой-то причине, когда вы используете ограничение rm
, которое дает компилятору свободу выбора, следует ли рассматривать делитель как операнд памяти или загружать его в регистр, Clang генерирует худший объектный код, чем если вы просто используете r
(что заставляет его загружать его в регистр). Это не влияет на GCC или ICC. Если вы заботитесь о качестве вывода на Clang, вы, вероятно, просто захотите использовать r
, так как это даст одинаково хороший объектный код для всех компиляторов.
Живая демонстрация в Godbolt Compiler Explorer
(Примечание: GCC использует в выводе SAL
мнемонику вместо метрики SHL
. Это идентичные инструкции - разница имеет значение только для правых сдвигов - и все программисты с программированием на звание используют SHL
. не знаю, почему GCC испускает SAL
, но вы можете просто мысленно преобразовать его в SHL
.)