C ++ стек и область видимости - PullRequest
8 голосов
/ 27 августа 2009

Я попробовал этот код на Visual C ++ 2008, и он показывает, что A и B не имеют одинаковый адрес.

int main()
{
    {
        int A;
        printf("%p\n", &A);
    }

    int B;
    printf("%p\n", &B);
}

Но так как A больше не существует, когда B определен, мне кажется, что это же место в стеке может быть использовано повторно ...

Я не понимаю, почему компилятор, похоже, не выполняет то, что выглядит как очень простая оптимизация (что может иметь значение, например, в контексте больших переменных и рекурсивных функций). И не похоже, что его повторное использование будет тяжелее как для процессора, так и для памяти. У кого-нибудь есть объяснение этому?

Я полагаю, что ответ на этот вопрос "потому что это гораздо сложнее, чем кажется", но, честно говоря, я понятия не имею.

edit : некоторые уточнения относительно ответов и комментариев ниже.

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

То, что я предлагаю, - это оптимизация памяти, но я не вижу, как это повредит производительности.

И, кстати, это происходит в сборках , все оптимизации будут включены.

Ответы [ 7 ]

8 голосов
/ 27 августа 2009

Повторное использование стекового пространства для локальных пользователей - это очень распространенная оптимизация. Фактически, в оптимизированной сборке, если вы не берете адрес локальных элементов, компилятор может даже не выделять пространство стека, а переменная будет жить только в регистре.

Возможно, эта оптимизация не будет выполняться по нескольким причинам.

Во-первых, если оптимизации отключены (например, отладочная сборка), компилятор не сделает ни одного из них, чтобы упростить отладку - вы можете просмотреть значение A, даже если оно больше не используется в функции.

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

Можно также представить компилятор, который не будет использовать эту оптимизацию, если пространство стека, используемое функцией, не превышает некоторый порог. Я не знаю ни одного компилятора, который бы делал это, поскольку повторное использование пространства локальных переменных, которые больше не используются, имеет нулевую стоимость и может быть применено повсеместно.

Если рост стека представляет серьезную проблему для вашего приложения, т. Е. В некоторых сценариях вы сталкиваетесь с переполнением стека, вам не следует полагаться на оптимизацию компилятором пространства стека. Вы должны рассмотреть возможность перемещения больших буферов в стеке в кучу и работать для устранения очень глубокой рекурсии. Например, в потоках Windows по умолчанию используется стек размером 1 МБ. Если вас беспокоит переполнение, поскольку из-за того, что вы выделяете 1 КБ памяти на каждый кадр стека и выполняете 1000 рекурсивных вызовов на глубину, исправление состоит не в том, чтобы пытаться уговорить компилятор сэкономить место на каждом кадре стека.

3 голосов
/ 27 августа 2009

Почему бы не проверить сборку?

Я немного изменил ваш код, так что int A = 1; и int B = 2; чтобы немного легче было расшифровать.

Из g ++ с настройками по умолчанию:

    .globl main
    .type   main, @function
main:
.LFB2:
    leal    4(%esp), %ecx
.LCFI0:
    andl    $-16, %esp
    pushl   -4(%ecx)
.LCFI1:
    pushl   %ebp
.LCFI2:
    movl    %esp, %ebp
.LCFI3:
    pushl   %ecx
.LCFI4:
    subl    $36, %esp
.LCFI5:
    movl    $1, -8(%ebp)
    leal    -8(%ebp), %eax
    movl    %eax, 4(%esp)
    movl    $.LC0, (%esp)
    call    printf
    movl    $2, -12(%ebp)
    leal    -12(%ebp), %eax
    movl    %eax, 4(%esp)
    movl    $.LC0, (%esp)
    call    printf
    movl    $0, %eax
    addl    $36, %esp
    popl    %ecx
    popl    %ebp
    leal    -4(%ecx), %esp
    ret
.LFE2:

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

Уведомление A назначается, а затем печатается. Затем B назначается и печатается, как в оригинальном источнике. Конечно, если вы используете разные настройки компилятора, это может выглядеть совсем иначе.

2 голосов
/ 27 августа 2009

Насколько я знаю, место для B зарезервировано при входе в main, а не в строке

int B;

Если вы прервете отладчик до этой строки, вы, тем не менее, сможете получить адрес B. Указатель стека также не изменяется после этой строки. Единственное, что происходит в этой строке, это то, что вызывается конструктор B.

1 голос
/ 28 августа 2009

Большая часть моей работы - борьба с компиляторами, и я должен сказать, что они не всегда делают то, что мы, люди, ожидаем от них. Даже если вы запрограммировали компилятор, вы все равно можете быть удивлены результатами, входную матрицу невозможно предсказать на 100%.

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

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

1 голос
/ 27 августа 2009

A выделяется в стеке после B. B объявлен после A в коде (что, кстати, не разрешено C90), но он все еще находится в верхней области основной функции и таким образом, существует от начала главного до конца. Таким образом, B нажимается, когда запускается основной, A нажимается, когда вводится внутренняя область, и выталкивается, когда он покидается, а затем, когда B покидает основную функцию, нажимается B.

1 голос
/ 27 августа 2009

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

0 голосов
/ 28 августа 2009

Компилятор действительно не имеет выбора в этом случае. Он не может предполагать какое-либо конкретное поведение printf(). В результате он должен предполагать, что printf() может зависеть от &A, пока существует сам А. Следовательно, само А находится во всей области видимости, в которой оно определено.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...