Почему не делает gcc, удаляют эту проверку энергонезависимой переменной?

Этот вопрос является главным образом академическим. Я спрашиваю из любопытства, не потому что это создает фактическую проблему для меня.

Рассмотрите следующую неправильную программу C.

#include <signal.h>
#include <stdio.h>

static int running = 1;

void handler(int u) {
    running = 0;
}

int main() {
    signal(SIGTERM, handler);
    while (running)
        ;
    printf("Bye!\n");
    return 0;
}

Эта программа является неправильной, потому что обработчик прерывает процесс выполнения программы, таким образом, running может быть изменен в любое время и должен поэтому быть объявлен volatile. Но скажем, программист забыл это.

gcc 4.3.3, с -O3 флаг, компилирует тело цикла (после того, как одна начальная проверка running флаг) вниз к бесконечному циклу

.L7:
        jmp     .L7

который должен был ожидаться.

Теперь мы помещаем что-то тривиальное в while цикл, как:

    while (running)
        putchar('.');

И внезапно, gcc больше не оптимизирует условие цикла! Блок тела цикла теперь похож на это (снова в -O3):

.L7:
        movq    stdout(%rip), %rsi
        movl    $46, %edi
        call    _IO_putc
        movl    running(%rip), %eax
        testl   %eax, %eax
        jne     .L7

Мы видим это running перезагружается из памяти каждый раз через цикл; это даже не кэшируется в регистре. По-видимому, gcc теперь думает что значение running возможно, изменился.

Итак, почему gcc внезапно решает, что должен перепроверить значение running в этом случае?

10
задан Thomas 25 March 2010 в 18:48
поделиться

4 ответа

В общем случае компилятору сложно точно знать, к каким объектам функция может иметь доступ и, следовательно, потенциально может изменять. В момент вызова putchar () GCC не знает, может ли быть реализация putchar () , которая могла бы изменить запущенный , поэтому он должен быть несколько пессимистичным и предполагать, что бег на самом деле мог быть изменен.

Например, в модуле перевода может быть реализация putchar () позже:

int putchar( int c)
{
    running = c;
    return c;
}

Даже если в модуле перевода нет реализации putchar () , там может быть что-то, что может, например, передать адрес работающего объекта , чтобы putchar мог его изменить:

void foo(void)
{
    set_putchar_status_location( &running);
}

Обратите внимание, что ваш обработчик () Функция доступна глобально, поэтому putchar () может вызвать сам handler () (напрямую или иначе), что является примером вышеупомянутой ситуации.

С другой стороны, поскольку запущенный виден только трансляционной единице (являющейся статической ), к тому времени, когда компилятор дойдет до конца файла, он сможет чтобы определить, что у putchar () нет возможности получить к нему доступ (при условии, что это так), и компилятор может вернуться и «исправить» пессимизацию в цикле while.

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

Обратите внимание, что в вашем первом примере, хотя то, что я упомянул в предыдущем абзаце, по-прежнему верно, компилятор может оптимизировать доступ к , запущенному , потому что «модель абстрактной машины», на которой основан язык C не учитывает асинхронную активность, за исключением очень ограниченных обстоятельств (одним из которых является ключевое слово volatile , а другим - обработка сигналов, хотя требования к обработке сигналов недостаточно сильны, чтобы помешать компилятору возможность оптимизировать доступ к работающему в вашем первом примере).

На самом деле, вот что C99 говорит о поведении абстрактной машины в этих точных обстоятельствах:

5.1.2.3/8 «Выполнение программы»

ПРИМЕР 1:

Реализация может определять один - однозначное соответствие между абстрактной и фактической семантикой: в каждой точке последовательности значения фактических объектов будут согласовываться со значениями, указанными в абстрактной семантике. Ключевое слово volatile тогда будет избыточным.

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

Наконец, вы должны отметить, что в стандарте C99 также говорится:

7.14.1.1/5 «Функция signal ]`

Если сигнал возникает не в результате вызова abort или raise , поведение не определено, если обработчик сигнала обращается к любому объекту со статической продолжительностью хранения, кроме присвоения значения объекту, объявленному как volatile sig_atomic_t ...

Строго говоря, переменную running может потребоваться объявить как:

volatile sig_atomic_t running = 1;
9
ответ дан 3 December 2019 в 20:40
поделиться

GCC, вероятно, предполагает, что вызов putchar может изменить любую глобальную переменную, включая работающую .

Обратите внимание на атрибут функции pure , который указывает, что функция не имеет побочных эффектов для глобального состояния. Я подозреваю, что если вы замените putchar () вызовом «чистой» функции, GCC повторно введет оптимизацию цикла.

3
ответ дан 3 December 2019 в 20:40
поделиться

Спасибо всем за ваши ответы и комментарии. Они были очень полезны, но ни один из них не дает полной картины. [Edit: Ответ Майкла Бурра теперь дает, делая этот ответ несколько излишним]. Я подведу итог здесь.

Хотя running статичен, handler не статичен; поэтому он может быть вызван из putchar и таким образом изменить running. Поскольку реализация putchar на данный момент не известна, можно предположить, что она может вызвать handler из тела цикла while.

Предположим, что обработчик был бы статическим. Можем ли мы тогда оптимизировать проверку выполнения? Ответ - нет, потому что реализация signal также находится вне этой единицы компиляции. Насколько известно gcc, signal может где-то хранить адрес handle (что, собственно, и происходит), а putchar может затем вызвать handler через этот указатель, даже если у него нет прямого доступа к этой функции.

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

Это объясняет, почему забывание volatile на практике не является такой большой проблемой, как может показаться на первый взгляд.

1
ответ дан 3 December 2019 в 20:40
поделиться

Поскольку вызов putchar () может изменить значение запущенного (GCC знает только, что putchar () - это внешняя функция, и она не знает, что она делает - поскольку GCC знает, что putchar () может вызвать handler () ).

4
ответ дан 3 December 2019 в 20:40
поделиться
Другие вопросы по тегам:

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