C编译器如何实现返回大型结构的函数?

C编译器如何实现返回大型结构的函数?,c,compiler-optimization,calling-convention,abi,compiler-theory,C,Compiler Optimization,Calling Convention,Abi,Compiler Theory,函数的返回值通常存储在堆栈或寄存器中。但对于大型结构,它必须在堆栈上。对于这段代码,在一个真正的编译器中需要进行多少复制?还是优化了 例如: struct Data { unsigned values[256]; }; Data createData() { Data data; // initialize data values... return data; } (假设函数不能内联。)无;没有复印件 调用方的数据返回值的地址实际上是作为隐藏参数传递给函数

函数的返回值通常存储在堆栈或寄存器中。但对于大型结构,它必须在堆栈上。对于这段代码,在一个真正的编译器中需要进行多少复制?还是优化了

例如:

struct Data {
    unsigned values[256];
};

Data createData() 
{
    Data data;
    // initialize data values...
    return data;
}

(假设函数不能内联。)

无;没有复印件

调用方的数据返回值的地址实际上是作为隐藏参数传递给函数的,createData函数只是写入调用方的堆栈帧

这就是我们所知道的。另请参阅

商业级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指定任何参数,所以这很奇怪,直到您意识到这是指向父数据版本的秘密隐藏指针

typedef struct {
    unsigned value[256];
} Data;

Data createData(void) {
    Data r;
    calcualte(&r);
    return r;
}

Data d = createData();
msvc(6,8,9)和gcc mingw(3.4.5,4.4.0)将生成类似以下伪代码的代码

void createData(Data* r) {
      calculate(&r)
}
Data d;
createData(&d);

linux上的gcc将发出memcpy()将结构复制回调用方的堆栈上。如果函数具有内部链接,则可以进行更多优化。

给出了许多示例,但基本上

这个问题没有明确的答案。它将取决于编译器。 C不指定从函数返回的结构的大小

下面是针对一个特定编译器的一些测试,即x86 RHEL 5.4上的gcc 4.1.2

一般情况下,没有复制 gcc更现实的情况,在堆栈上分配,memcpy给调用方 此外,VS2008(将上面编译为C)将在createData()的堆栈上保留结构数据,并在调试模式下执行
rep movsd
循环将其复制回调用方,在发布模式下,它将把rand()的返回值(%eax)直接移回调用方

但对于大型结构,它必须位于堆堆栈上

的确如此!在堆栈上分配声明为局部变量的大型结构。很高兴把事情弄清楚

至于避免复制,正如其他人所指出的:

  • 大多数调用约定通过传递一个附加参数来处理“函数返回结构”,该参数指向调用方堆栈框架中应该放置结构的位置。这绝对是呼叫约定的问题,而不是语言的问题

  • 使用这种调用约定,即使是相对简单的编译器也可以注意到代码路径何时肯定会返回结构,并修复对该结构成员的赋值,以便它们直接进入调用方的框架,而不必复制。关键是编译器要注意通过函数的所有终止代码路径都返回相同的结构变量。如果是这种情况,编译器可以安全地使用调用方框架中的空间,从而消除在返回点复制的需要


如果我的编辑不是您想要的,请告诉我。这是一个很糟糕的例子,因为“数据”本质上是“&data[0]”的同义词。指针是标量。我更喜欢把它推给程序员而不是编译器。大型对象应始终通过指针传递。这清楚地表明大对象没有被复制。“没有复制”有点夸大了这一点。结构仍然需要复制。即使“createData()”只修改了数据结构中的1个元素,整个结构仍会被复制回调用方一次—这可以通过查看生成的程序集轻松验证。(至少适用于需要遵守平台ABI的非静态函数。静态函数或具有这些函数的编译器的大型程序优化器有更多优化。说真的,没有复制。我已检查生成的程序集。@all:我们只说它取决于。因为它确实取决于。上面的简单情况是optimized,编译时已知所有值。请尝试在循环中填充所有256个值,例如data.values[i]=rand()。或其他一些外部函数.gcc(4.4.1 atlest,带-O2)它将返回给调用方。GCC不会为上面的简单案例THUG生成一个MeMCPY。其他编译器可能会有不同。更多的信息,问题被标记。如果你将代码编译为C++,GCC将不会为Sealt返回生成副本。(但是如果编译的数组的所有值都是从编译时未知的源来填充的),问题是标记为C,但是代码显然是C++。在使用数据类型之前,请注意缺失的结构。GCC将使用“数据”的堆栈空间。在createData函数中,它不会执行您显示的整个转换,尽管它会传递一个“指针”调用方的结构位置。@nos返回大型结构的函数的实现方式不是标准指定的,因此没有正确的答案。我提到的编译器确实会生成这样的代码:在调用方帧上分配空间并将其地址传递给被调用方。他只是完全编写了代码吗?或者这是在这些编译器上实际发生了什么?他怎么知道的?实际上有两种不同的调用方传递地址约定,这取决于是否需要调用的方法
[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
#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
        .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