Я пытаюсь понять смысл System V AMD64 - ABI соглашениео вызовах и смотрю на следующий пример:
struct Vec3{
double x, y, z;
};
struct Vec3 do_something(void);
void use(struct Vec3 * out){
*out = do_something();
}
A Vec3 -variable имеет тип MEMORY, и, следовательно, вызывающая сторона (use) должна выделить место для возвращаемой переменной и передать его как скрытый указатель на вызываемый объект (т.е. do_something). Вот что мы видим в получающемся ассемблере (на Godbolt, скомпилированном с -O2):
use:
pushq %rbx
movq %rdi, %rbx ;remember out
subq $32, %rsp ;memory for returned object
movq %rsp, %rdi ;hidden pointer to %rdi
call do_something
movdqu (%rsp), %xmm0 ;copy memory to out
movq 16(%rsp), %rax
movups %xmm0, (%rbx)
movq %rax, 16(%rbx)
addq $32, %rsp ;unwind/restore
popq %rbx
ret
Я понимаю, что псевдоним указателя out (например, как глобальная переменная) может использоваться в do_something, и поэтому out не может быть передан как скрытый указатель на do_something: если это так, out будет изменено внутри do_something, а не когда do_something вернется, поэтому некоторые вычисления могут стать ошибочными. Например, эта версия do_something будет возвращать ошибочные результаты:
struct Vec3 global; //initialized somewhere
struct Vec3 do_something(void){
struct Vec3 res;
res.x = 2*global.x;
res.y = global.y+global.x;
res.z = 0;
return res;
}
если out где псевдоним для глобальной переменной global и использовался как скрытый указатель, переданный в %rdi, res также был псевдонимом global, потому что компилятор будет использовать память, указанную для непосредственно скрытым указателем (своего рода RVO в C), без фактического создания временного объекта и копирования его при возврате, тогда res.y будет 2*x+y (если x,y - старые значения global), а не x+y как и для любого другого скрытого указателя.
Мне было предложено, что использование restrict должно решить проблему, т.е.
void use(struct Vec3 *restrict out){
*out = do_something();
}
потому что теперь компилятор знает, что в do_something нет псевдонимов out, поэтому ассемблер может быть таким простым:
use:
jmp do_something ; %rdi is now the hidden pointer
Однако это не относится ни к gcc, ни к clang - ассемблер остается неизменным (см. godbolt).
Что мешает использовать out в качестве скрытого указателя?
Примечание: желаемое (или очень похожее) поведение будет достигнуто для слегка отличающейся сигнатуры функции:
struct Vec3 use_v2(){
return do_something();
}
что приводит к (см. крестник):
use_v2:
pushq %r12
movq %rdi, %r12
call do_something
movq %r12, %rax
popq %r12
ret