Мы считаем, что мы используем 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
как инструкция сериализации (например, ничего не будет сделано с выводом инструкции). Прежде всего, 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 неинициализированной приведет к загрузке мусора из стека или обнулению регистра, вместо того, чтобы просто использовать регистр, не записав его вначале.
Других решений пока нет …