MSVC Inline ASM для GCC

Я пытаюсь обрабатывать как компиляторы MSVC, так и GCC, обновляя эту базу кода для работы с GCC. Но я точно не знаю, как работает GCC встроенный ASM. Теперь я не очень хорош в переводе ASM на C, иначе я бы просто использовал C вместо ASM.

SLONG Div16(signed long a, signed long b)
{
    signed long v;
#ifdef __GNUC__ // GCC doesnt work.
__asm() {
#else // MSVC
__asm {
#endif
        mov edx, a
        mov ebx, b          
        mov eax, edx           
        shl eax, 16          
        sar edx, 16            
        idiv ebx              
        mov v, eax              
    }
    return v;
}

signed long ROR13(signed long val)
{
    _asm{ 
        ror val, 13
    }
}

Я предполагаю, что ROR13 работает что-то вроде (val << 13) | (val >> (32 - 13)), но код не производит тот же вывод.

Каков правильный способ перевода этой встроенной ASM в GCC и/или что такое C-перевод этого кода?

Ответ 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.)