Засорение памяти в двух вызовах встроенной сборки по сравнению с одним вызовом встроенной сборки?

Этот вопрос следует за этим one, учитывая GCC-совместимый компилятор и x86-64 архитектуру. .

Мне интересно, есть ли разница между option 1, option 2 и option 3 ниже. Будет ли результат одинаковым во всех контекстах или будет отличаться? И если да, то в чем будет разница?

// Option 1
asm volatile(:::"memory");
asm volatile("CPUID":"=a"(eax),"=b"(ebx),"=c"(ecx),"=d"(edx):"0"(level):);

а также

// Option 2
asm volatile("CPUID":"=a"(eax),"=b"(ebx),"=c"(ecx),"=d"(edx):"0"(level):);
asm volatile(:::"memory");

а также

// Option 3
asm volatile("CPUID":"=a"(eax),"=b"(ebx),"=c"(ecx),"=d"(edx):"0"(level):"memory");

person Vincent    schedule 30.01.2018    source источник
comment
2 и 3 должны быть одинаковыми. 1 вызовет перезагрузку level из памяти, если она там окажется.   -  person Jester    schedule 30.01.2018
comment
@Jester: вариант 2 позволит изменить порядок самого CPUID с несвязанными загрузками/сохранениями, не относящимися к volatile. Скорее всего, это не то, что вам нужно.   -  person Peter Cordes    schedule 31.01.2018
comment
@Jester: на самом деле перезагружается только в том случае, если его адрес сбежал из функции или что-то в этом роде. Он не вызывает перезагрузку, если он был в памяти как аргумент стека или обычный локальный пролит из-за давления на регистр.   -  person Peter Cordes    schedule 31.01.2018


Ответы (1)


Варианты 1 и 2 позволяют переупорядочивать сам CPUID с несвязанными загрузками/сохранениями, не относящимися к volatile (в том или ином направлении). Скорее всего, это не то, что вам нужно.

Вы можете установить барьер памяти с обеих сторон CPUID, но, безусловно, лучше просто сделать CPUID барьером памяти.


Как указывает Jester, вариант 1 заставит перезагрузить level из памяти, если его адрес когда-либо передавался вне функции или если он уже является глобальным или static.

(Или какой-либо точный критерий, который решает, может ли переменная C быть изменена для чтения или записи с помощью ассемблера, который использует "memory" clobber. Я думаю, что это по существу то же самое, что оптимизатор использует, чтобы решить, может ли переменная храниться в регистре через невстроенный вызов функции для непрозрачной функции, поэтому чистые локальные переменные, адрес которых никуда не передается и которые не являются входными данными для оператора asm, могут по-прежнему находиться в регистрах).

Например (Проводник компилятора Godbolt):

void foo(int level){
    int eax, ebx, ecx, edx;
    asm volatile("":::"memory");
    asm volatile("CPUID"
        :  "=a"(eax),"=b"(ebx),"=c"(ecx),"=d"(edx)
        :  "0"(level)
        :
    );
}

# x86-64 gcc7.3  -O3 -fverbose-asm

    pushq   %rbx  #           # rbx is call-preserved, but we clobber it.
    movl    %edi, %eax      # level, eax
    CPUID
    popq    %rbx    #
    ret

Обратите внимание на отсутствие сброса/перезагрузки функции arg.

Обычно я бы использовал синтаксис Intel, но со встроенным ассемблером рекомендуется всегда использовать AT&T, если вы не используете синтаксис ненависти AT&T или не знаете его.

Даже если он запускается в памяти (соглашение о вызовах i386 System V с аргументами стека), компилятор все равно решает, что ничто другое (включая оператор asm с затиранием памяти) не может ссылаться на него. Но как определить разницу между задержкой загрузки? Измените функцию arg перед барьером, а затем используйте ее после:

void modify_level(int level){
    level += 1;                  // modify level before the barrier
    int eax, ebx, ecx, edx;
    asm volatile("#mem barrier here":::"memory");
    asm volatile("CPUID"         // then read it after
    :  "=a"(eax),"=b"(ebx),"=c"(ecx),"=d"(edx)
    :  "0"(level):);
}

Ассемблерный вывод из gcc -m32 -O3 -fverbose-asm:

modify_level(int):
    pushl   %ebx  #
    #mem barrier here
    movl    8(%esp), %eax   # level, tmp97
    addl    $1, %eax        #, level
    CPUID
    popl    %ebx    #
    ret

Обратите внимание, что компилятор позволяет переупорядочивать level++ через барьер памяти, потому что это локальная переменная.

Godbolt фильтрует написанные от руки комментарии asm вместе со строками комментариев asm, сгенерированными компилятором. Я отключил фильтр комментариев и нашел барьер памяти. Вы можете удалить -fverbose-asm, чтобы получить меньше шума. Или используйте строку без комментариев для барьера памяти: ее не нужно собирать, если вы просто смотрите на вывод компилятора asm. (Если вы не используете clang со встроенным ассемблером).


Кстати, исходная версия вашего вопроса не скомпилировалась: вы не указали пустую строку в качестве шаблона asm. asm(:::"memory"). Разделы output, input и clobber могут быть пустыми, но строка инструкции asm необязательна.

Забавный факт, вы можете поместить ассемблерные комментарии в строку:

asm volatile("# memory barrier here":::"memory");

gcc заполняет все %whatever вещей в строковом шаблоне по мере записи ассемблерного вывода, так что вы даже можете делать что-то вроде "CPUID # %%0 was in %0" и смотреть, что gcc выбрал для ваших "фиктивных" аргументов, которые иначе не упоминаются в ассемблерном шаблоне. (Более интересно, когда фиктивные операнды ввода/вывода памяти сообщают компилятору, какую память вы читаете/записываете, вместо использования "memory" клобера, когда вы даете оператору asm указатель.)

person Peter Cordes    schedule 31.01.2018
comment
Обычно я бы использовал синтаксис Intel — если вы не создаете заголовок для библиотеки, рассмотрите возможность использования диалекты. Идея заключается в том, что в качестве заголовка вы не можете знать, создается ли файл, в который вы входите, с -masm=intel или att. - person David Wohlferd; 31.01.2018