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

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

Например:

struct Data {
    unsigned values[256];
};

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

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

Ответы [ 5 ]

22 голосов
/ 28 января 2010
нет

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

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

Это известно как названная оптимизация возвращаемого значения . Также см. c ++ faq по этой теме .

коммерческие компиляторы 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.

7 голосов
/ 29 января 2010

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

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

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

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

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

6 голосов
/ 29 января 2010

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

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

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 для копирования его обратно вызывающей стороне в режиме отладки, в режиме Release он переместит возвращаемое значение rand () (% eax) непосредственно обратно к вызывающей стороне

4 голосов
/ 28 января 2010
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);
1 голос
/ 28 января 2010

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

...