Как компиляторы C реализуют функции, возвращающие большие структуры?
-
23-09-2019 - |
Вопрос
Возвращаемое значение функции обычно хранится в стеке или регистре.Но для большой структуры она должна находиться в стеке.Сколько копий должно произойти в реальном компиляторе для этого кода?Или это оптимизировано?
Например:
struct Data {
unsigned values[256];
};
Data createData()
{
Data data;
// initialize data values...
return data;
}
(Предполагая, что функция не может быть встроена..)
Решение
Никто;никакие копии не делаются.
Адрес возвращаемого значения Data вызывающего объекта фактически передается в качестве скрытого аргумента функции, а функция createData просто записывает в кадр стека вызывающего объекта.
Это известно как именованная оптимизация возвращаемого значения.Также см. Часто задаваемые вопросы по C++ по этой теме.
Компиляторы C++ коммерческого уровня реализуют возврат по значению таким образом, чтобы исключить накладные расходы, по крайней мере, в простых случаях.
...
Когда yourCode() вызывает rbv(), компилятор тайно передает указатель на место, где rbv() должен создать «возвращенный» объект.
Вы можете продемонстрировать, что это было сделано, добавив в вашу структуру деструктор с printf.Деструктор следует вызывать только один раз, если действует оптимизация возврата по значению, в противном случае — дважды.
Также вы можете проверить сборку, чтобы увидеть, что это происходит:
Data createData()
{
Data data;
// initialize data values...
data.values[5] = 6;
return data;
}
вот сборка:
__Z10createDatav:
LFB2:
pushl %ebp
LCFI0:
movl %esp, %ebp
LCFI1:
subl $1032, %esp
LCFI2:
movl 8(%ebp), %eax
movl $6, 20(%eax)
leave
ret $4
LFE2:
Любопытно, что для элемента данных было выделено достаточно места в стеке. subl $1032, %esp
, но учтите, что он принимает первый аргумент в стеке 8(%ebp)
в качестве базового адреса объекта, а затем инициализирует элемент 6 этого элемента.Поскольку мы не указали никаких аргументов для createData, это любопытно, пока вы не поймете, что это секретный скрытый указатель на родительскую версию Data.
Другие советы
Но для большой структуры она должна находиться на
кучакуча.
Действительно так!Большая структура, объявленная как локальная переменная, размещается в стеке.Рад, что это прояснилось.
Что касается предотвращения копирования, как отмечали другие:
Большинство соглашений о вызовах касаются «функции, возвращающей структуру», передавая дополнительный параметр, указывающий место в кадре стека вызывающего объекта, в котором должна быть размещена структура.Это определенно вопрос соглашения о вызовах, а не языка.
Благодаря этому соглашению о вызовах даже относительно простой компилятор становится возможным заметить, когда путь кода определенно будет возвращать структуру, и исправить назначения членам этой структуры, чтобы они попадали непосредственно в кадр вызывающего объекта и не возвращались. его не нужно копировать.Главное, чтобы компилятор заметил, что все завершающие пути кода через функцию возвращают такой же структурная переменная.В этом случае компилятор может безопасно использовать пространство во фрейме вызывающего объекта, устраняя необходимость копирования в точке возврата.
Приведено много примеров, но в основном
Этот вопрос не имеет однозначного ответа.это будет зависеть от компилятора.
C не определяет, насколько большие структуры возвращаются из функции.
Вот несколько тестов для одного конкретного компилятора: gcc 4.1.2 на x86 RHEL 5.4.
gcc тривиальный случай, копирование не требуется
[00:05:21 1 ~] $ gcc -O2 -S -c t.c
[00:05:23 1 ~] $ cat t.s
.file "t.c"
.text
.p2align 4,,15
.globl createData
.type createData, @function
createData:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax
movl $1, 24(%eax)
popl %ebp
ret $4
.size createData, .-createData
.ident "GCC: (GNU) 4.1.2 20080704 (Red Hat 4.1.2-46)"
.section .note.GNU-stack,"",@progbits
gcc более реалистичный случай, выделение в стеке, memcpy для вызывающего абонента
#include <stdlib.h>
struct Data {
unsigned values[256];
};
struct Data createData()
{
struct Data data;
int i;
for(i = 0; i < 256 ; i++)
data.values[i] = rand();
return data;
}
[00:06:08 1 ~] $ gcc -O2 -S -c t.c
[00:06:10 1 ~] $ cat t.s
.file "t.c"
.text
.p2align 4,,15
.globl createData
.type createData, @function
createData:
pushl %ebp
movl %esp, %ebp
pushl %edi
pushl %esi
pushl %ebx
movl $1, %ebx
subl $1036, %esp
movl 8(%ebp), %edi
leal -1036(%ebp), %esi
.p2align 4,,7
.L2:
call rand
movl %eax, -4(%esi,%ebx,4)
addl $1, %ebx
cmpl $257, %ebx
jne .L2
movl %esi, 4(%esp)
movl %edi, (%esp)
movl $1024, 8(%esp)
call memcpy
addl $1036, %esp
movl %edi, %eax
popl %ebx
popl %esi
popl %edi
popl %ebp
ret $4
.size createData, .-createData
.ident "GCC: (GNU) 4.1.2 20080704 (Red Hat 4.1.2-46)"
.section .note.GNU-stack,"",@progbits
gcc 4.4.2### сильно разросся и не копируется для описанного выше нетривиального случая.
.file "t.c"
.text
.p2align 4,,15
.globl createData
.type createData, @function
createData:
pushl %ebp
movl %esp, %ebp
pushl %edi
pushl %esi
pushl %ebx
movl $1, %ebx
subl $1036, %esp
movl 8(%ebp), %edi
leal -1036(%ebp), %esi
.p2align 4,,7
.L2:
call rand
movl %eax, -4(%esi,%ebx,4)
addl $1, %ebx
cmpl $257, %ebx
jne .L2
movl %esi, 4(%esp)
movl %edi, (%esp)
movl $1024, 8(%esp)
call memcpy
addl $1036, %esp
movl %edi, %eax
popl %ebx
popl %esi
popl %edi
popl %ebp
ret $4
.size createData, .-createData
.ident "GCC: (GNU) 4.1.2 20080704 (Red Hat 4.1.2-46)"
.section .note.GNU-stack,"",@progbits
Кроме того, VS2008 (скомпилированный выше как C) зарезервирует структуру Data в стеке createData() и выполнит rep movsd
цикл, чтобы скопировать его обратно вызывающему объекту в режиме отладки, в режиме выпуска он переместит возвращаемое значение rand() (%eax) непосредственно обратно вызывающему объекту.
typedef struct {
unsigned value[256];
} Data;
Data createData(void) {
Data r;
calcualte(&r);
return r;
}
Data d = createData();
мсвк (6,8,9) и GCC mingw(3.4.5,4.4.0) сгенерирует код, подобный следующему псевдокоду
void createData(Data* r) {
calculate(&r)
}
Data d;
createData(&d);
gcc в Linux выполнит memcpy(), чтобы скопировать структуру обратно в стек вызывающего объекта.Однако если функция имеет внутреннюю связь, становятся доступны дополнительные оптимизации.