Вы сказали компилятору, что собираетесь поместить вывод в %0
, и он может выбрать любой регистр для этого "=r"
. Но вместо этого вы никогда не пишете %0
в своем шаблоне.
И вы используете два временных значения без видимой причины, когда вы могли бы использовать %0
в качестве временного.
Как обычно, вы можетеотладьте свой встроенный asm, добавив комментарии типа # 0 = %0
и просмотрев выходные данные asm компилятора. (Не разбирать, просто gcc -S
, чтобы увидеть, что он заполняет. Например, # 0 = %ecx
. (Вы не использовали ранний клоббер "=&r"
, поэтому он может выбрать тот же регистр, что и входные данные).
Кроме того, здесь есть еще 2 ошибки:
не компилируется. Запрос 2 разных операндов в ECX с ограничениями "c"
не может работать, если компилятор не может доказать при компиляции-время, когда они имеют одинаковое значение, так что %1
и %2
могут быть одинаковыми регистрами. https://godbolt.org/z/LgR4xS
Вы разыменовываете указатели ввода, не сообщая компилятору, что вы читаете указатель напамять. * Использовать "memory"
операнды-заглушки или фиктивные запоминания. Как я могу указать, что можно использовать память *, на которую указывает * встроенный аргумент ASM?
Или лучше https://gcc.gnu.org/wiki/DontUseInlineAsm, потому что это бесполезно для этого; просто позвольте GCC самому запускать загрузки movzb. unsigned char*
безопасен от строго псевдонимов UB, поэтому вы можете безопасно привести любой указатель к unsigned char*
и разыщите его, даже не используя memcpy
или другие хаки для борьбыt против языковых правил для более широкого доступа с выравниванием или без ввода текста.
Но если вы настаиваете на встроенном asm, читайте руководства и учебные пособия, ссылки на https://stackoverflow.com/tags/inline-assembly/info. Вы не можете просто бросить коду стены, пока он не придерживается встроенного asm: вы должны понять, почему ваш код безопасен, чтобы надеяться на его безопасность. Есть много способов, как встроенный asm может сработать, но на самом деле не работает или ждет, чтобы сломаться с другим окружающим кодом.
Это безопасная и не совсем ужасная версия (кроме неизбежной оптимизации). поражение частей встроенного асма). Вам все еще нужна загрузка movzbl для обеих загрузок, даже если возвращаемое значение составляет всего 8 бит. movzbl
- это естественный эффективный способ загрузки байта, заменяющий вместо слияния старое содержимое полного регистра.
unsigned char read(void *index, void *data)
{
uintptr_t value;
asm (
" movzb (%[idx]), %k[out] \n\t"
" movzb (%[arr], %[out]), %k[out]\n"
: [out] "=&r" (value) // early-clobber output
: [idx] "r" (index), [arr] "r" (data)
: "memory" // we deref some inputs as pointers
);
return value;
}
Обратите внимание на ранний клоббер на выходе: это останавливает gcc от выборатот же регистр для вывода, что и один из входов. Было бы безопасно уничтожить регистр [idx]
при первой загрузке, но я не знаю, как сообщить GCC об этом в одном операторе asm. Вы можете разделить ваш оператор asm на два отдельных, каждый со своими операндами ввода и вывода, соединяя выход первого с входом второго через локальную переменную. Тогда никому не понадобится ранний клоббер, потому что они просто обертывают отдельные инструкции, такие как встроенный синтаксис asm GNU C, предназначенный для хорошей работы.
Godbolt с тестовым вызывающим абонентом, чтобы увидетьон вызывается / оптимизирует при двойном вызове, с i386 clang и x86-64 gcc. например, запрос index
в регистре заставляет LEA вместо того, чтобы позволить компилятору увидеть разыскивание и позволить ему выбрать режим адресации для *index
. Также дополнительные movzbl %al, %eax
, сделанные компилятором при добавлении к unsigned sum
, потому что мы использовали узкий тип возврата.
Я использовал uintptr_t value
, так что это может компилироваться для 32-битных и 64-битныхx86. Нет ничего плохого в том, чтобы сделать вывод из оператора asm более широким, чем возвращаемое значение функции, и это избавляет нас от необходимости использовать модификаторы размера, такие как movzbl (%1), %k0
, чтобы GCC печатал 32-битное имя регистра(например, EAX), если он выбрал AL для 8-битной выходной переменной, например.
Я действительно решил использовать %k[out]
в интересах 64-битного режима: мы хотим movzbl (%rdi), %eax
, а неmovzb (%rdi), %rax
(потеря префикса REX).
Вы также можете объявить функцию, возвращающую unsigned int
или uintptr_t
, поэтому компилятор знает, что ему не нужно возвращать нулевое расширение . OTOH иногда может помочь компилятору узнать, что диапазон значений составляет всего 0..255. Вы можете сказать, что вы производите значение с нулевым расширением, используя if(retval>255) __builtin_unreachable()
или что-то еще. Или вы можете просто не использовать встроенный asm .
Вам не нужно asm volatile
. (Предполагая, что вы хотите позволить ему оптимизировать, если результат не используется, или быть выведенным из циклов для постоянных входов). Вам нужен только "memory"
клоббер, поэтому, если он все-таки привыкнет, компилятор знает, что он читает память.
("memory"
клоббер считается как вся память, являющаяся входом, и вся память, являющаяся выходом. Поэтому он не может использовать CSE, например, вывод из цикла, поскольку, поскольку компилятор знает, что один вызов может прочитать что-то, что написал предыдущий, на практике "memory"
клоббер примерно так же плох, как asm volatile
. Даже двапоследовательные вызовы этой функции без прикосновения к массиву ввода вынуждают компилятор дважды выдавать инструкции.)
Этого можно избежать с помощью фиктивных операндов ввода в память, чтобы компилятор знал, что этот блок asm не изменить память, только читать ее. Но если вы действительно заботитесь об эффективности, вам не следует использовать встроенный ассемблер для этого.
Но, как я сказал, нет никаких причин использовать встроенный ассемблер:
Это подойдетТо же самое в 100% портативном и безопасном ISO C:
// safe from strict-aliasing violations
// because unsigned char* can alias anything
inline
unsigned char read(void *index, void *data) {
unsigned idx = *(unsigned char*)index;
unsigned char * dp = data;
return dp[idx];
}
Вы можете привести один или оба указателя к volatile unsigned char*
, если вы настаиваете на том, чтобы доступ происходил каждый раз и не был оптимизирован.
Или, может быть, даже до atomic<unsigned char> *
в зависимости от того, что вы делаете. (Это хак, предпочтение C ++ 20 atomic_ref
для атомарной загрузки / хранения объектов, которые обычно не являются атомарными.)