#c #assembly #simd #inline-assembly #cpu-registers
#c #сборка #simd #встроенная сборка #cpu-регистры
Вопрос:
Если у меня есть какая-то нестроевая функция, и компилятор C знает, что эта функция изменяет некоторые регистры, тогда компилятор сохранит все необходимые регистры перед выполнением вызова функции.
По крайней мере, я ожидаю, что компилятор сделает это (сохранение), насколько он знает, какие регистры будут изменены внутри вызываемой функции.
Теперь представьте, что моя функция изменяет ВСЕ возможные регистры процессора (общего назначения, SIMD, FPU и т.д.). Как я могу заставить компилятор сохранить все, что ему нужно, перед выполнением любого ВЫЗОВА этой функции? Напомним, что моя функция не является встроенной, т. Е. Вызывается через инструкцию ВЫЗОВА.
Конечно, с помощью asm я могу поместить все возможные регистры в стек при запуске моей функции и вернуть все регистры обратно перед возвратом функции.
Хотя я могу сохранить ВСЕ возможные регистры, я бы предпочел, чтобы компилятор сохранял только необходимые регистры, которые использовались вызывающей функцией, по соображениям производительности (скорости) и использования памяти.
Поскольку внутри моей функции я заранее не знаю, кто будет ее использовать, поэтому я должен сохранить все возможные регистры. Но в том месте, где использовалась моя функция, компилятор точно знает, какие регистры используются в вызывающей функции, следовательно, это может сэкономить гораздо меньше необходимых регистров, потому что наверняка будут использоваться не все регистры.
Поэтому я хочу пометить свою функцию как «изменение всех регистров», чтобы компилятор C отправлял в стек только те регистры, которые ему нужны, перед вызовом моей функции.
Есть ли какой-либо способ сделать это? Любой атрибут GCC / CLang / MSVC функции? Или, может быть, перечисление всех регистров в разделе clobber asm
инструкции?
Главное, что я не хочу сам сохранять регистры внутри этой функции (по какой-то конкретной причине), вместо этого я хочу, чтобы все вызывающие сохраняли все необходимые регистры перед вызовом моей функции, но я хочу, чтобы все вызывающие пользователи знали, что моя функция изменяет все, что возможно.
Я ищу какой-то воображаемый атрибут modifies-all, например:
__attribute__((modifies_all_registers)) void f();
Я провел следующий эксперимент:
__attribute__((noinline)) int modify(int i) {
asm volatile(
""
: " m" (i) ::
"rax", "rbx", "rcx", "rdx", "rsi", "rdi", "rbp", "rsp",
"r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15",
"xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7",
"xmm8", "xmm9", "xmm10", "xmm11", "xmm12", "xmm13", "xmm14", "xmm15",
"ymm0", "ymm1", "ymm2", "ymm3", "ymm4", "ymm5", "ymm6","ymm7",
"ymm8", "ymm9", "ymm10", "ymm11", "ymm12", "ymm13", "ymm14", "ymm15",
"zmm0", "zmm1", "zmm2", "zmm3", "zmm4", "zmm5", "zmm6", "zmm7",
"zmm8", "zmm9", "zmm10", "zmm11", "zmm12", "zmm13", "zmm14", "zmm15"
);
return i 1;
}
int main(int argc, char ** argv) {
auto volatile x = modify(argc);
}
другими словами, я заблокировал почти все возможные регистры, и компилятор сгенерировал следующую push-последовательность внутри modify()
(а также ту же pop-последовательность в конце):
push rbp
mov rbp, rsp
push r15
push r14
push r13
push r12
push rbx
больше ничего не было выдвинуто, поэтому я вижу, что каким-то образом компилятор (CLang) не заботился о других регистрах, кроме rbx, rbp, r12-r15
. Означает ли это, что существует какое-то соглашение о вызовах C , в котором говорится, что я могу изменять любые другие регистры, кроме этих нескольких, без их восстановления при возврате функции?
Комментарии:
1. Не доверяйте компилятору. Сохраните регистры перед их изменением и восстановите перед возвратом вашей функции.
2. @Arty: компилятор сохранит только те регистры перед вызовом функции, которые, как он знает, будут изменены. Обычно эти регистры предназначены для переданных параметров. Если вы посмотрите на язык ассемблера для функций, компилятор также сохранит регистры перед выполнением первого оператора. Таким образом, компилятор сохраняет некоторые регистры перед вызовом и некоторые регистры перед первым оператором. Итак, ваш код должен сохранять регистры, которые вы изменяете в своей функции. Компилятор при вызове вашей функции понятия не имеет, какие регистры будут использоваться внутри вашей функции.
3. Компилятор имеет соглашение о вызовах в отношении регистров. Предполагается, что некоторые регистры общего назначения и не будут сохранены. Это может также включать регистры FPU и SIMD. Вы должны изучить соглашение о вызовах компилятора. В языке ассемблера правила безопасности говорят, что для сохранения регистров вы будете изменять и восстанавливать их до завершения функции.
4. Возможно, вы захотите спросить себя, действительно ли вам нужно сохранять все эти регистры сразу. Поскольку у вас есть ограничения на сохранение регистров, возможно, используйте только пару регистров. Так, например, ARM имеет 16 регистров. Вместо того, чтобы использовать все 16 одновременно, используйте только 4 одновременно. Я могу использовать 4 других регистра в качестве регистров «сохранения». Все это зависит от компилятора, и вам нужно будет ознакомиться с документацией компилятора, а также с набором инструкций процессора , чтобы узнать, действительно ли вам нужно сохранять все эти регистры сразу.
5. Наконец, если в вашем коде нет места для сохранения всех этих регистров, что заставляет вас думать, что это делает компилятор?
Ответ №1:
Означает ли это, что существует какое-то соглашение о вызовах C , в котором говорится, что я могу изменять любые другие регистры, кроме этих нескольких, без их восстановления при возврате функции?
ДА. Среди прочего, спецификация ABI, используемая на данной платформе, определяет соглашения о вызовах функций. Соглашения о вызовах определяют набор регистров, которые могут быть заблокированы функцией, и набор регистров, которые должны быть сохранены функцией. Если регистры первого набора содержат полезные данные для вызывающего, ожидается, что вызывающий сохранит эти данные перед вызовом. Если в вызываемой функции должны использоваться регистры из последнего набора, функция должна сохранить и восстановить эти регистры перед возвратом.
Существуют также соглашения относительно того, какие регистры, в каком порядке используются для передачи аргументов функции и получения возвращаемого значения. Вы можете считать эти регистры заблокированными, поскольку вызывающий должен инициализировать их значениями параметров (и, таким образом, сохранить любые полезные данные, которые были в этих регистрах до вызова), и вызываемому абоненту разрешено их изменять.
В вашем случае оператор asm помечает все регистры как заблокированные, и компилятор сохраняет и восстанавливает только те регистры, которые требуется сохранить при вызове функции. Обратите внимание, что по умолчанию вызывающий объект всегда сохраняет регистры из набора clobber перед вызовом функции, независимо от того, были ли они фактически изменены функцией или нет. В некоторых случаях оптимизатор может удалить сохранение регистров, которые фактически не были изменены — например, если вызов функции встроен или компилятор способен анализировать тело функции (например, в случае LTO). Однако, если тело функции неизвестно во время компиляции, компилятор должен предположить худшее и придерживаться спецификации ABI.
Итак, в общем, вам не нужно отмечать функцию каким-либо особым образом — правила ABI уже работают таким образом, что регистры сохраняются и восстанавливаются по мере необходимости. И, как вы сами видели, даже с помощью инструкций asm компиляторы могут определить, какие регистры используются в функции. Если вы по какой-то причине все еще хотите сохранить определенные или все регистры, ваш единственный вариант — писать на ассемблере. Или, в случае, если вы реализуете какое-то переключение контекста, используйте специализированные инструкции, такие как XSAVE
/ XRSTOR
или API, подобные ucontext
.
Комментарии:
1. Хороший ответ! Проголосовал против.