MSVC Inline ASM для GCC

Я пытаюсь работать с компиляторами MSVC и GCC, обновляя эту кодовую базу для работы на GCC. Но я точно не знаю, как работает встроенный ASM в GCC. Теперь я не очень хорош в переводе 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));
}

(См. Также Лучшие практики для операций кругового сдвига (поворота) в C++).

Это будет иметь ту же семантику, что и ваш исходный код: ROR val, 13, Фактически, MSVC будет генерировать именно этот объектный код, как и GCC. (Лязг, интересно, сделаем 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 будет нарушать правило, как если бы, если компилятор не может доказать, что эти комбинации 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

(Примечание: GCC использует SAL мнемоника в его выводе, а не SHL Мнемоника. Это идентичные инструкции - разница имеет значение только для правых сдвигов - и все программисты, использующие вменяемые сборки SHL, Я понятия не имею, почему GCC испускает SAL, но вы можете просто мысленно преобразовать его в SHL.)

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