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

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

Например:

struct Data {
    unsigned values[256];
};

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

(Принятие функции не может быть встроено..)

24
задан Peter Cordes 18 October 2019 в 02:07
поделиться

5 ответов

Нет; Копии не сделаны.

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

Это известно как под названием «Оптимизация возврата» . Также см. FAQ C ++ FAQ по этой теме .

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

...

, когда ваш youcode () вызывает 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 этого элемента. Поскольку мы не указывали какие-либо аргументы для создания создания, это любопытно, пока вы не осознаете, что это секретный скрытый указатель на версию данных родителей.

23
ответ дан 28 November 2019 в 23:37
поделиться

Есть много приведенных примеров, но в основном

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

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

Вот некоторые тесты для одного конкретного компилятора, GCC 4.1.2 на X86 RHEL 5.4

Trivial Case RHEL, без копирования

[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 более реалистичные случаи, выделяют на стеке, мемкпина к абонеру

#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), будет зарезервировать данные структуры на стопку создателя () и выполнять PREP MOVSD , чтобы скопировать его обратно в звонящий в режим отладки, в режиме отпуска Режим он будет перемещать возвращаемое значение RAND () (% EAX) непосредственно обратно в звонящий

6
ответ дан 28 November 2019 в 23:37
поделиться
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);
4
ответ дан 28 November 2019 в 23:37
поделиться

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

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

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

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

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

7
ответ дан 28 November 2019 в 23:37
поделиться

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

1
ответ дан 28 November 2019 в 23:37
поделиться
Другие вопросы по тегам:

Похожие вопросы: