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

StackOverflow https://stackoverflow.com/questions/2155730

Вопрос

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

Например:

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(), чтобы скопировать структуру обратно в стек вызывающего объекта.Однако если функция имеет внутреннюю связь, становятся доступны дополнительные оптимизации.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top