Вы правы во всех отношениях, этот код полон лжи для компилятора, который может вас укусить. Например. с другим окружающим кодом или разными версиями / опциями компилятора (особенно оптимизация во время компоновки для включения встраивания между файлами).
swap_vbufs
даже не выглядит очень эффективным, я подозреваю, что gcc будет делать то же самое или лучше с чистой версией C. https://gcc.gnu.org/wiki/DontUseInlineAsm. stosd
- это 3 мопа на Intel, хуже, чем в обычном магазине mov
+ add rdi,4
. А если сделать add rdi,4
безусловным, можно избежать необходимости в этом блоке else
, который добавит дополнительный jmp
на (надеюсь) быстрый путь, где нет хранилища MMIO для видеопамяти, поскольку буферы были равны.
(lodsd
- это всего 2 мопа на Haswell и новее, так что это нормально, если вас не волнует IvyBridge или старше).
В коде ядра, я полагаю, они избегают SSE2, хотя он является базовым для x86-64, иначе вы, вероятно, захотите использовать это. Для обычного назначения памяти вам нужно просто memcpy
с rep movsd
или ERMSB rep movsb
, но я предполагаю, что суть в том, чтобы избегать хранилищ MMIO, когда это возможно, путем проверки кэшированной копии видеопамяти RAM. Тем не менее, безусловные потоковые хранилища с movnti
могут быть эффективными, если только видеопамять не сопоставлена с UC (без кэширования) вместо WC.
Легко построить примеры, где это действительно ломается на практике, например, снова используя соответствующую переменную C после встроенного оператора asm в той же функции. (Или в родительской функции, которая встроила asm).
Ввод, который вы хотите уничтожить, должен обрабатываться обычно фиктивным выводом или выводом RMW с переменной C tmp, а не просто "r"
. или "a"
.
"r"
или любое ограничение конкретного регистра, такое как "D"
, означает, что это вход только для чтения, и компилятор может ожидать, что значение будет восстановлено впоследствии. Нет ограничения «вход, который я хочу уничтожить»; Вы должны синтезировать это с фиктивным выводом или переменной.
Это все относится к другим компиляторам (clang и ICC), которые поддерживают встроенный синтаксис GNU C.
Из руководства GCC: Расширенный asm
Входные операнды :
Не изменять содержимое операндов только для ввода (кроме входов, связанных с выходами). Компилятор предполагает, что при выходе из оператора asm эти операнды содержат те же значения, что и до выполнения оператора. Невозможно использовать clobbers для информирования компилятора об изменении значений в этих входах.
(Clobber rax
делает ошибкой использование "a"
в качестве входа; клобберы и операнды не могут перекрываться.)
Пример для 1: зарегистрировать входные операнды
int plain_C(int in) { return (in+1) + in; }
int bad_asm(int in) {
int out;
asm ("inc %%edi;\n\t mov %%edi, %0" : "=a"(out) : [in]"D"(in) );
return out + in;
}
Скомпилировано в проводнике Godbolt
Обратите внимание, что gcc addl
использует edi
для in
, хотя встроенный asm уничтожил этот регистр . В этом случае он удерживает in+1
. Я использовал gcc9.1, но это не новое поведение.
## gcc9.1 -O3 -fverbose-asm
bad(int):
inc %edi;
mov %edi, %eax # out (comment mentions out because I used %0)
addl %edi, %eax # in, tmp86
ret
Мы исправляем это, сообщая компилятору, что тот же входной регистр также является выходом, поэтому он больше не может рассчитывать на это. (Или используя auto tmp = in; asm("..." : "+r"(tmp));
)
int safe(int in) {
int out;
int dummy;
asm ("inc %%edi;\n\t mov %%edi, %%eax"
: "=a"(out),
"=&D"(dummy)
: [in]"1"(in) // matching constraint, or "D" works.
);
return out + in;
}
# gcc9.1 again.
safe_asm(int):
movl %edi, %edx # tmp89, in compiler-generated save of in
# start inline asm
inc %edi;
mov %edi, %eax
# end inline asm
addl %edx, %eax # in, tmp88
ret
Очевидно, что "lea 1(%%rdi), %0"
позволит избежать проблем, если не изменять входные данные, как и mov
/ inc
. Это искусственный пример, который намеренно уничтожает ввод.
Если функция делает не встроенной и не использует входную переменную после оператора asm, вы обычно избегаете лжи компилятору, если это регистр с закрытым вызовом.
Нередко можно встретить людей, написавших небезопасный код, который работает в контексте, в котором они его используют. Также нередко они могут быть убеждены, что просто тестируют его в этом контексте с помощью одной версии / опций компилятора. достаточно для проверки его безопасности или правильности.
Но это не так, как работает asm; компилятор доверяет вам точно описать поведение asm и просто выполняет подстановку текста в части шаблона.
Было бы дурацкой пропущенной оптимизацией, если бы gcc предполагал, что операторы asm всегда уничтожают свои входные данные. Фактически, те же ограничения, которые использует inline asm, (я думаю) используются во внутренних файлах описания машин, которые учат gcc об ISA. (Таким образом, уничтоженные входные данные будут ужасны для Code-Gen).
Вся конструкция встроенного ассемблера GNU C основана на переносе одной инструкции, поэтому даже ранний клоббер для выходных данных не используется по умолчанию. При необходимости вы должны сделать это вручную, если пишете несколько инструкций или цикл внутри встроенного asm.
потенциальная ошибка, из-за которой компилятору не сообщили, что память, на которую указывают эти указатели, была прочитана или изменена.
Это тоже правильно. Операнд ввода регистра не подразумевает, что указанная память также является операндом ввода. В функции, которая не может быть встроенной, это на самом деле не может вызвать проблемы, но как только вы включите оптимизацию во время соединения, станет возможной перекрестная вставка в файл и межпроцедурная оптимизация.
Существует существующий Информирующий лязг, который при встроенной сборке читает определенную область памяти без ответа на вопрос. Эта ссылка Godbolt показывает некоторые способы, с помощью которых вы можете выявить эту проблему, например,
arr[2] = 1;
asm(...);
arr[2] = 0;
Если gcc предполагает, что arr[2]
не является вводом в asm, а только сам адрес arr
, он выполнит удаление мертвого хранилища и удалит присвоение =1
. (Или посмотрите, как он переупорядочивает магазин с помощью оператора asm, а затем сворачивает 2 магазина в одно и то же место).
Массив хорош тем, что показывает, что даже "m"(*arr)
не работает для указателя, только фактический массив . Этот операнд ввода только сообщит компилятору, что arr[0]
является вводом, но не arr[2]
. Это хорошо, если это все, что читает ваш asm, потому что он не блокирует оптимизацию других частей.
В этом примере memset
для правильного объявления, что указанная память является выходным операндом, приведите указатель на указатель на массив и разыменуйте его, чтобы сообщить gcc, что весь диапазон памяти является операндом , *(char (*)[count])pointer
. (Вы можете оставить []
пустым, чтобы указать область памяти произвольной длины, доступ к которой осуществляется с помощью этого указателя.)
// correct version written by @MichaelPetch.
void *memset(void *dest, int value, size_t count)
{
void *tmp = dest;
asm ("rep stosb # mem output is %2"
: "+D"(tmp), "+c"(count), // tell the compiler we modify the regs
"=m"(*(char (*)[count])tmp) // dummy memory output
: "a"(value) // EAX actually is read-only
: // no clobbers
);
return dest;
}
Включение asm-комментария с использованием фиктивного операнда позволяет нам увидеть, как компилятор разместил его. Мы видим, что компилятор выбирает (%rdi)
с синтаксисом AT & T, поэтому он готов использовать регистр, который также является операндом ввода / вывода.
Если на выходе был ранний клоббер, возможно, он хотел использовать другой регистр, но без этого нам ничего не стоило бы получить правильность.
С функцией void
, которая не возвращает указатель (или после встраивания в функцию, которая не использует возвращаемое значение), ей не нужно никуда копировать указатель arg, прежде чем позволить rep stosb
destroy это.