Подтвердить что ты не робот

Как компиляторы C реализуют функции, которые возвращают большие структуры?

Возвращаемое значение функции обычно сохраняется в стеке или в регистре. Но для большой структуры она должна быть в стеке. Сколько копий должно произойти в реальном компиляторе для этого кода? Или он оптимизирован?

Например:

struct Data {
    unsigned values[256];
};

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

(Предполагая, что функция не может быть встроена.)

4b9b3361

Ответ 1

Отсутствует; копии не выполняются.

Адрес вызывающего объекта Возвращаемое значение данных фактически передается как скрытый аргумент функции, а функция createData просто записывает в стек стека вызывающего абонента.

Это называется с именем return value optimization. Также см. С++ faq в этом разделе.

компиляторы коммерческого класса С++ реализуют возврат по значению таким образом, который позволяет им устранять накладные расходы, по крайней мере в простых случаях

...

Когда 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 было выделено достаточно места для элемента данных subl $1032, %esp, но обратите внимание, что он принимает первый аргумент в стеке 8(%ebp) в качестве базового адреса объекта и затем инициализирует элемент 6 этого элемента, Поскольку мы не указывали никаких аргументов createData, это любопытно, пока вы не осознаете, что это секретный скрытый указатель на родительскую версию Data.

Ответ 2

Но для большой структуры он должен находиться в стеке heap.

В самом деле! В стеке выделяется большая структура, объявленная как локальная переменная. Рад, что это прояснилось.

Как и во избежание копирования, как отмечали другие:

  • Большинство соглашений о вызовах относятся к "функции, возвращающей структуру", передавая дополнительный параметр, указывающий местоположение в кадре стека вызывающего объекта, в котором должна быть размещена структура. Это определенно вопрос для вызывающего соглашения, а не для языка.

  • С помощью этого соглашения о вызове становится возможным даже, когда относительно простой компилятор замечает, когда путь кода определенно собирается вернуть структуру, и чтобы он исправлял назначения этим членам структуры, чтобы они переходили непосредственно в кадра вызывающего абонента и не нужно копировать. Ключ заключается в том, чтобы компилятор заметил, что все конечные пути кода через функцию возвращают одну и ту же структурную переменную. В этом случае компилятор может безопасно использовать пространство в кадре вызывающего абонента, исключая необходимость в копии в точке возврата.

Ответ 3

Существует много примеров, но в основном

Этот вопрос не имеет определенного ответа. это будет зависеть от компилятора.

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) зарезервирует struct Data в стеке createData() и выполнит цикл rep movsd, чтобы скопировать его обратно в вызывающий в режиме Debug, в режиме Release он переместит возвращаемое значение rand() (% eax) непосредственно обратно вызывающему абоненту

Ответ 4

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);

Ответ 5

gcc on linux выдаст memcpy(), чтобы скопировать структуру обратно в стек вызывающего. Если функция имеет внутреннюю связь, тем больше становится доступнее оптимизация.