Я знаю, что при использовании встроенной сборки gcc, если вы не укажете иное, предполагается, что вы используете все свои входные данные, прежде чем писать какой-либо выходной операнд. Если вы действительно хотите записать выходной операнд перед использованием всех входных данных, вы должны указать его как раннее закрытие, чтобы он не использовал повторно этот регистр для ввода.
У меня вопрос возник, когда я увидел этот пример из авторитетного справочника:
void
dscal (size_t n, double *x, double alpha)
{
asm ("/* lots of asm here */"
: "+m" (*(double (*)[n]) x), "+&r" (n), "+b" (x) // <-- There's the "+&r" (n)
: "d" (alpha), "b" (32), "b" (48), "b" (64),
"b" (80), "b" (96), "b" (112)
: "cr0",
"vs32","vs33","vs34","vs35","vs36","vs37","vs38","vs39",
"vs40","vs41","vs42","vs43","vs44","vs45","vs46","vs47");
}
Что? Почему он рано забивает регистр ввода-вывода? Разве это не тот же регистр?
На этой странице нет никаких объяснений по этому вопросу.
Копнув дальше, я нашел это, в котором говорится:
Операнд, считываемый инструкцией, может быть привязан к операнду раннего удаления, если его использование в качестве входных данных происходит только до записи раннего результата. Добавление альтернатив этой формы часто позволяет GCC создавать более качественный код, когда ранний клоббер может повлиять только на некоторые операнды чтения. См., например, insn «mulsi3» ARM.
Более того, если операнд EarlyClobber также является операндом чтения/записи, то этот операнд записывается только после его использования.
В последнем говорится о случае +&r
, но я, честно говоря, не понимаю, о чем там говорится. Я не знаю, что значит «использованный».
Быстрый просмотр grep -r '+&'
ядра Linux дал очень мало результатов и только один файл, в котором оно используется в архитектуре x86 (это то, с чем я немного знаком (не слишком)): (файлarch/x86/crypto/curve25519 -x86_64.c)
/* Computes the addition of four-element f1 with value in f2
* and returns the carry (if any) */
static inline u64 add_scalar(u64 *out, const u64 *f1, u64 f2)
{
u64 carry_r;
asm volatile(
/* Clear registers to propagate the carry bit */
" xor %%r8d, %%r8d;"
" xor %%r9d, %%r9d;"
" xor %%r10d, %%r10d;"
" xor %%r11d, %%r11d;"
" xor %k1, %k1;"
/* Begin addition chain */
" addq 0(%3), %0;"
" movq %0, 0(%2);"
" adcxq 8(%3), %%r8;"
" movq %%r8, 8(%2);"
" adcxq 16(%3), %%r9;"
" movq %%r9, 16(%2);"
" adcxq 24(%3), %%r10;"
" movq %%r10, 24(%2);"
/* Return the carry bit in a register */
" adcx %%r11, %1;"
: "+&r"(f2), "=&r"(carry_r)
: "r"(out), "r"(f1)
: "%r8", "%r9", "%r10", "%r11", "memory", "cc");
return carry_r;
}
Я действительно не понимаю, почему использования +r
будет недостаточно.
@DavidWohlferd Вот и все! Я ценю ваше время. Я написал несколько надуманных примеров, чтобы создать такую ситуацию, и использование +&
изменило ситуацию. Подобные детали кажутся нечеткими и малоизвестными. Для всех, кому интересно, я нашел эту тему по моему вопросу. Кстати, почему бы не ответить на это? Он отлично ответил на мой вопрос!
Поскольку мой комментарий оказался полезным, предлагаю его в качестве ответа:
Что, если при входе в asm компилятор узнает, что f2 и f1 содержат одно и то же значение? Может ли он использовать один и тот же регистр для обоих? Это может сработать (таким образом сохраняя регистр), если f1 используется только до записи f2. Но если это невозможно гарантировать, Earlyclobber гарантирует использование отдельных регистров.
У компилятора есть стимул (производительности) минимизировать использование регистров при вызове asm. Чем больше регистров он использует, тем больше регистров необходимо очистить/восстановить.
Еще добавлю, что, как правило, следует избегать использования встроенного ассемблера. Несмотря на то, что это круто, мощно и интересно, действительно сложно сделать все правильно и больно поддерживать.
Это определенно очень круто 😎. И еще, как вы думаете, для разработки ОС следует использовать встроенный ассемблер или просто использовать отдельные файлы?
Вот пример, если вы хотите добавить его к своему ответу: godbolt.org/z/e9Goe8Eve
Разработка ОС сложна. Вы имеете дело с серьёзными проблемами синхронизации, вплоть до тактов и инструкций, которые ни одна другая программа на языке C никогда не будет использовать. Хотя я бы рекомендовал свести к минимуму использование встроенного ассемблера, полностью избегать его может быть непрактично.
Что, если при входе в ассемблер компилятору известно, что оба
f2
иf1
содержат одно и то же значение? Может ли он использовать один и тот же регистр для обоих? Это может сработать (таким образом сохраняя регистр), если f1 используется только до записи f2. Но если это невозможно гарантировать, Earlyclobber гарантирует использование отдельных регистров.