C 编译器如何实现返回大型结构的函数?
函数的返回值通常存储在堆栈或寄存器中。但对于大型结构,它必须位于堆栈上。对于这段代码,在真实的编译器中需要进行多少复制?还是已经优化掉了?
例如:(
struct Data {
unsigned values[256];
};
Data createData()
{
Data data;
// initialize data values...
return data;
}
假设函数不能内联..)
The return value of a function is usually stored on the stack or in a register. But for a large structure, it has to be on the stack. How much copying has to happen in a real compiler for this code? Or is it optimized away?
For example:
struct Data {
unsigned values[256];
};
Data createData()
{
Data data;
// initialize data values...
return data;
}
(Assuming the function cannot be inlined..)
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

发布评论
评论(5)
中二柚2024-08-26 07:52:39
给出的例子很多,但基本上
这个问题没有明确的答案。这取决于编译器。
C 没有指定从函数返回多大的结构。
以下是针对某个特定编译器的一些测试,x86 RHEL 5.4
gcc 上的 gcc 4.1.2 简单情况,不复制
[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)将在createData()的堆栈上保留struct Data,并执行rep movsd
循环将其在调试模式下复制回调用者,在发布模式下它将 rand() (%eax) 的返回值直接移回调用者
失退2024-08-26 07:52:39
但对于大型结构,它必须位于
堆堆栈上。
确实如此!声明为局部变量的大型结构在堆栈上分配。很高兴事情已经解决了。
至于避免复制,正如其他人所指出的:
大多数调用约定通过传递一个附加参数来处理“函数返回结构”,该参数指向调用者堆栈帧中应放置该结构的位置。这绝对是调用约定的问题,而不是语言的问题。
- 通过这种调用约定,即使是相对简单的编译器也可以注意到代码路径何时肯定会返回结构,并修复对该结构成员的赋值,以便它们直接进入调用者的框架并且不必复制。关键是编译器要注意通过函数的所有终止代码路径都返回相同结构变量。如果是这种情况,编译器可以安全地使用调用者框架中的空间,从而无需在返回点进行复制。
~没有更多了~
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
- 这个问题没有明确的答案。这取决于编译器。
- gcc 上的 gcc 4.1.2 简单情况,不复制
- gcc 更现实的情况,在堆栈上分配,memcpy 到调用者
- gcc 4.4.2### 增长了很多,并且不复制对于上述非平凡的情况。
- This question does not have any definite answer. it will depend on the compiler.
- gcc trivial case, no copying
- gcc more realistic case , allocate on stack, memcpy to caller
- gcc 4.4.2### has grown a lot, and does not copy for the above non-trivial case.
没有任何;没有完成任何副本。
调用者的 Data 返回值的地址实际上作为隐藏参数传递给函数,并且 createData 函数只是写入调用者的堆栈帧。
这称为命名返回值优化。另请参阅有关此主题的 c++ 常见问题解答。
您可以通过向结构中添加带有 printf 的析构函数来证明这是已完成的。如果按值返回优化正在运行,则析构函数只能调用一次,否则调用两次。
您还可以检查程序集以查看是否发生这种情况:
这是程序集:
奇怪的是,它在堆栈上为数据项
subl $1032, %esp
分配了足够的空间,但请注意,它采用第一个参数放在堆栈上8(%ebp)
作为对象的基地址,然后初始化该项目的元素 6。由于我们没有为 createData 指定任何参数,因此这很奇怪,直到您意识到这是指向父级数据版本的秘密隐藏指针。None; no copies are done.
The address of the caller's Data return value is actually passed as a hidden argument to the function, and the createData function simply writes into the caller's stack frame.
This is known as the named return value optimisation. Also see the c++ faq on this topic.
You can demonstrate that this has been done by adding a destructor with a printf to your struct. The destructor should only be called once if this return-by-value optimisation is in operation, otherwise twice.
Also you can check the assembly to see that this happens:
here's the assembly:
Curiously, it allocated enough space on the stack for the data item
subl $1032, %esp
, but note that it takes the first argument on the stack8(%ebp)
as the base address of the object, and then initialises element 6 of that item. Since we didn't specify any arguments to createData, this is curious until you realise this is the secret hidden pointer to the parent's version of Data.