Использование встроенной сборки с инструкциями сериализации

Мы считаем, что мы используем GCC (или же GCCсовместимый) компилятор на X86_64 архитектура, и это eax, ebx, ecx, edx а также level переменные (unsigned int или же unsigned int*) для ввода и вывода инструкции (например, Вот).

asm("CPUID":::);
asm volatile("CPUID":::);
asm volatile("CPUID":::"memory");
asm volatile("CPUID":"=a"(eax),"=b"(ebx),"=c"(ecx),"=d"(edx)::"memory");
asm volatile("CPUID":"=a"(eax):"0"(level):"memory");
asm volatile("CPUID"::"a"(level):"memory"); // Not sure of this syntax
asm volatile("CPUID":"=a"(eax),"=b"(ebx),"=c"(ecx),"=d"(edx):"0"(level):"memory");
asm("CPUID":"=a"(eax),"=b"(ebx),"=c"(ecx),"=d"(edx):"0"(level):"memory");
asm volatile("CPUID":"=a"(eax),"=b"(ebx),"=c"(ecx),"=d"(edx):"0"(level));
  • Я не привык к синтаксису встроенной сборки, и мне интересно, какова будет разница между всеми этими вызовами в контексте, где я просто хочу использовать CPUID как инструкция сериализации (например, ничего не будет сделано с выводом инструкции).
  • Могут ли некоторые из этих вызовов привести к ошибкам?
  • Какой из этих вызовов был бы наиболее подходящим (учитывая, что я хочу наименьшее количество накладных расходов, насколько это возможно, но в то же время «самая сильная» возможная сериализация)?

1

Решение

Прежде всего, lfence может быть так же сильно сериализовать, как cpuid, а может и нет. Если вы заботитесь о производительности, проверьте и посмотрите, можете ли вы найти доказательства того, что lfence достаточно силен (по крайней мере, для вашего случая использования). Возможно даже используя оба mfence; lfence может быть лучше, чем cpuidесли ни mfence ни lfence одного достаточно для сериализации на AMD и Intel. (Я не уверен, см. Мой связанный комментарий).


2. Да, все те, которые не сообщают компилятору, что оператор asm пишет E [A-D] X, опасны и, вероятно, вызовут странные для отладки странности. (т.е. вы должны использовать (фиктивные) выходные операнды или клобберы).

Тебе нужно volatileпотому что вы хотите, чтобы asm-код выполнялся для побочного эффекта сериализации, а не для вывода результатов.

Если вы не хотите использовать результат CPUID для чего-либо (например, делайте двойную работу, сериализуя а также запрашивая что-то), вы должны просто перечислить регистры как клобберы, а не выходные данные, поэтому вам не нужны никакие переменные Си для хранения результатов.

// volatile is already implied because there are no output operands
// but it doesn't hurt to be explicit.

// Serialize and block compile-time reordering of loads/stores across this
asm volatile("CPUID"::: "eax","ebx","ecx","edx", "memory");

// the "eax" clobber covers RAX in x86-64 code, you don't need an #ifdef __i386__

Мне интересно, в чем будет разница между всеми этими звонками

Прежде всего, ни один из них не является «звонком». Они асм заявления, и встроить в функцию, где вы их используете. Сам CPUID также не является «вызовом», хотя я думаю, вы могли бы рассматривать его как вызов функции микрокода, встроенной в CPU. Но по этой логике каждая инструкция является «вызовом», например, mul rcx принимает входные данные в RAX и RCX и возвращает в RDX: RAX.


Первые три (и последний без выходов, просто level вход) уничтожить RAX через RDX, не сказав компилятору. Предполагается, что эти регистры все еще хранят то, что они хранили в них. Они явно непригодны.


asm("CPUID":"=a"(eax),"=b"(ebx),"=c"(ecx),"=d"(edx):"0"(level):"memory"); (тот без volatile) будет оптимизировать, если вы не используете какой-либо из выходов. И если вы их используете, его все равно можно поднять из петель. Неvolatile Оператор asm обрабатывается оптимизатором как чистая функция без побочных эффектов. https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html#index-asm-volatile

У него есть память, но (я думаю), которая не останавливает его от оптимизации, это просто означает, что если / когда / где он делает run, любые переменные, которые он может читать / записывать, синхронизируются с памятью, поэтому содержимое памяти соответствует тому, что абстрактная машина C имела бы в этой точке. Это может исключить местных жителей, которые не получили свой адрес, хотя.

asm("" ::: "memory") очень похоже на std::atomic_thread_fence(std::memory_order_seq_cst)но обратите внимание, что это asm оператор не имеет выходов и, следовательно, неявно volatile, Это почему он не оптимизирован, а не из-за "memory" сам клоп А (volatile) Оператор asm с затвором памяти является барьером компилятора для переупорядочивания нагрузок или хранения через него.

Оптимизатору абсолютно все равно, что находится внутри первого строкового литерала, только ограничения / клобберы, поэтому asm volatile("anything" ::: register clobbers, "memory") также является барьером памяти только во время компиляции. Я предполагаю, что вы хотите сериализовать некоторые операции с памятью.


"0"(level) ограничение соответствия для первого операнда ( "=a"). Вы могли бы в равной степени написать "a"(level)потому что в этом случае у компилятора нет выбора, какой регистр выбрать; выходное ограничение может быть удовлетворено только eax, Вы могли бы также использовать "+a"(eax) в качестве выходного операнда, но тогда вам придется установить eax=level до утверждения asm. Соответствующие ограничения вместо операндов чтения-записи иногда необходимы для стека x87; Я думаю, что однажды возникла такая проблема. Но помимо таких странных вещей, преимущество заключается в возможности использовать различные переменные C для ввода и вывода или вообще не использовать переменную для ввода. (например, литеральная константа или lvalue (выражение)).

В любом случае, указание компилятору предоставить ввод, вероятно, приведет к дополнительной инструкции, например level=0 приведет к xorобнуление eax, Это было бы напрасной тратой инструкции, если бы ей еще не требовался нулевой регистр. Обычно обнуление входных данных по xor нарушает зависимость от предыдущего значения, но весь смысл CPUID в том, что это сериализация, поэтому он должен дождаться завершения всех предыдущих инструкций. Убедиться eax рано готов бессмысленно; если вы не заботитесь о выходных данных, даже не говорите компилятору, что ваш оператор asm принимает входные данные. Компиляторы затрудняют или делают невозможным использование неопределенного / неинициализированного значения без издержек; иногда оставление переменной C неинициализированной приведет к загрузке мусора из стека или обнулению регистра, вместо того, чтобы просто использовать регистр, не записав его вначале.

2

Другие решения

Других решений пока нет …