Как я могу сказать, что во встроенной сборке Clang / LLVM x86-64 я перекрыл состояние x87 / media? - PullRequest
0 голосов
/ 14 декабря 2018

Я пишу некоторую встроенную сборку x86-64, которая может повлиять на состояние с плавающей запятой и носителя (SSE, MMX и т. Д.), Но мне не хочется самому сохранять и восстанавливать состояние.Есть ли у Clang / LLVM ограничение clobber для этого?

(я не слишком знаком с архитектурой x86-64 или встроенной сборкой, поэтому было трудно понять, что искатьБолее подробная информация на случай, если это проблема XY: я работаю над простой библиотекой сопрограмм в Rust. Когда мы переключаем задачи, нам нужно сохранить старое состояние процессора и загрузить новое состояние, и я хотел бы написать какНебольшая сборка, насколько это возможно. Я предполагаю, что позволить компилятору позаботиться о сохранении и восстановлении состояния - это самый простой способ сделать это.)

1 Ответ

0 голосов
/ 14 декабря 2018

Если ваша сопрограмма выглядит как непрозрачный (не встроенный) вызов функции, компилятор уже будет предполагать, что состояние FP забито (за исключением регистров управления, таких как MXCSR и управляющего слова x87 (режим округления)), потому что все регистры FP закрыты при вызове в обычном соглашении о вызовах функций.

За исключением Windows, где xmm6..15 сохраняются при вызове.


Также имейте в виду, что если вы помещаете call во встроенный ассемблер, вы не сможете сказать компилятору, что ваш ассемблер перекрывает красную зону (на 128 байт ниже RSP в x86-64 System V ABI).Вы можете скомпилировать этот файл с помощью -mno-redzone или использовать add rsp, -128 перед call, чтобы пропустить красную зону, принадлежащую сгенерированному компилятором коду.


Чтобы объявить клобберы насостояние FP, вы должны называть все регистры по отдельности.

"xmm0", "xmm1", ..., "xmm15" (сгусток xmm0 считается как сглаживающий ymm0 / zmm0).

Для правильной меры вы должны также назвать "mm0", ..., "mm7" (MMX), на случай, если ваш код встроен в какой-то унаследованный код с использованием встроенных функций MMX.

Чтобы также сжать стек x87, "st" - это то, как вы ссылаетесь на st(0) в списке клоббера.Остальные регистры имеют свои нормальные имена для синтаксиса GAS: "st (1)", ..., "st (7)" . https://stackoverflow.com/questions/39728398/how-to-specify-clobbered-bottom-of-the-x87-fpu-stack-with-extended-gcc-assembly You never know, it is possible to compile with clang -mfpmath = 387 , or to use 387 via long double`.

(Надеемся, что ни один код не использует -mfpmath=387 в 64-битном режиме и встроенных MMX одновременно; следующий тестовый пример выглядит немного неработающим с gcc в этом случае.)

#include <immintrin.h>
float gvar;
int testclobber(float f, char *p)
{
    int arg1 = 1, arg2 = 2;

    f += gvar;  // with -mno-sse, this will be in an x87 register
    __m64 mmx_var = *(const __m64*)p;             // MMX
    mmx_var = _mm_unpacklo_pi8(mmx_var, mmx_var);

    // x86-64 System V calling convention
    unsigned long long retval;
    asm volatile ("add $-128, %%rsp \n\t"   // skip red zone.  -128 fits in an imm8
                  "call whatever \n\t"
                  "sub $-128, %%rsp  \n\t"
                 // FIXME should probably align the stack in here somewhere

                 : "=a"(retval)            // returns in RAX
                 : "D" (arg1), "S" (arg2)  // input args in registers

                 : "rcx", "rdx", "r8", "r9", "r10", "r11"  // call-clobbered integer regs
                  // call clobbered FP regs, *NOT* including MXCSR
                  , "mm0", "mm1", "mm2", "mm3", "mm4", "mm5", "mm6", "mm7"           // MMX
                  , "st", "st(1)", "st(2)", "st(3)", "st(4)", "st(5)", "st(6)", "st(7)"  // x87
                  // SSE/AVX: clobbering any results in a redundant vzeroupper with gcc?
                  , "xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7"
                  , "xmm8", "xmm9", "xmm10", "xmm11", "xmm12", "xmm13", "xmm14", "xmm15"
                 #ifdef __AVX512F__
                  , "zmm16", "zmm17", "zmm18", "zmm19", "zmm20", "zmm21", "zmm22", "zmm23"
                  , "zmm24", "zmm25", "zmm26", "zmm27", "zmm28", "zmm29", "zmm30", "zmm31"
                  , "k0", "k1", "k2", "k3", "k4", "k5", "k6", "k7"
                 #endif
                 #ifdef __MPX__
                , "bnd0", "bnd1", "bnd2", "bnd3"
                #endif

                , "memory"  // reads/writes of globals and pointed-to data can't reorder across the asm (at compile time; runtime StoreLoad reordering is still a thing)
         );

    // Use the MMX var after the asm: compiler has to spill/reload the reg it was in
    *(__m64*)p = mmx_var;
    _mm_empty();   // emms

    gvar = f;  // memory clobber prevents hoisting this ahead of the asm.

    return retval;
}

source + asm в проводнике компилятора Godbolt

Комментируя одну из строк clobbers, мы можем видеть, что разлив-улет исчезает в ассемблере,например, комментирование x87 st .. st(7) clobbers приводит к коду, который оставляет f + gvar в st0, всего на fst dword [gvar] после вызова.

Аналогично, комментирование строки mm0 позволяет gcc и clang сохранять mmx_varв mm0 через call. ABI требует, чтобы FPU находился в режиме x87, а не MMX, на call / ret, этого на самом деле недостаточно. Компилятор разлит / перезагрузит вокруг asm, но он выиграл 'Вставьте emms для нас. Но, к тому же, для функции, использующей MMX, было бы ошибкой вызывать вашу подпрограмму без предварительного выполнения _mm_empty(), поэтому, возможно, это не является реальной проблемой.

Я не экспериментировал с __m256 переменными, чтобы увидеть, вставляет ли он vzeroupper перед asm, чтобы избежать возможных замедлений SSE / AVX.

Если мы прокомментируем строку xmm8..15, мы увидимверсия, которая не использует x87 для float, сохраняет ее в xmm8, потому что теперь она думает, что у нее есть некоторые неубитые регистры xmm. Если мы прокомментируем оба набора строк, предполагается, что xmm0 живет через asm, так что это работает как тест clobbers.


вывод asm со всемиClobbers на месте

Сохраняет / восстанавливает RBX (для удержания аргумента указателя в операторе asm), что приводит к повторному выравниванию стека на 16. Это еще одна проблема с использованием call из встроенногоasm: я не думаю, что выравнивание RSP гарантировано.

# from clang7.0 -march=skylake-avx512 -mmpx
testclobber:                            # @testclobber
    push    rbx
    vaddss  xmm0, xmm0, dword ptr [rip + gvar]
    vmovss  dword ptr [rsp - 12], xmm0 # 4-byte Spill   (because of xmm0..15 clobber)
    mov     rbx, rdi                    # save pointer for after asm
    movq    mm0, qword ptr [rdi]
    punpcklbw       mm0, mm0        # mm0 = mm0[0,0,1,1,2,2,3,3]
    movq    qword ptr [rsp - 8], mm0 # 8-byte Spill    (because of mm0..7 clobber)
    mov     edi, 1
    mov     esi, 2
    add     rsp, -128
    call    whatever
    sub     rsp, -128

    movq    mm0, qword ptr [rsp - 8] # 8-byte Reload
    movq    qword ptr [rbx], mm0
    emms                                     # note this didn't happen before call
    vmovss  xmm0, dword ptr [rsp - 12] # 4-byte Reload
    vmovss  dword ptr [rip + gvar], xmm0
    pop     rbx
    ret

Обратите внимание, что из-за "memory" clobber в операторе asm, *p и gvar читаются перед asm, но написано после.Без этого оптимизатор мог бы уменьшить нагрузку или поднять хранилище, чтобы ни одна локальная переменная не находилась в выражении asm.Но теперь оптимизатору необходимо предположить, что сам оператор asm может прочитать старое значение gvar и / или изменить его.(И предположим, что p указывает на память, которая так или иначе доступна глобально, потому что мы не использовали __restrict.)

...