Почему gcc не удаляет эту проверку энергонезависимой переменной? - PullRequest
10 голосов
/ 25 марта 2010

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

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

#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?

Ответы [ 5 ]

9 голосов
/ 25 марта 2010

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

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

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

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

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

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

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

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

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

На самом деле, вот что 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;
4 голосов
/ 25 марта 2010

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

3 голосов
/ 25 марта 2010

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

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

1 голос
/ 27 октября 2011

putchar может изменить running.

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

1 голос
/ 25 марта 2010

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

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

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

Так, в каких случаях можно * оптимизировать проверку running? Кажется, что это возможно только в том случае, если тело цикла не вызывает никаких функций извне этого модуля преобразования, так что во время компиляции известно, что происходит, а не происходит внутри тела цикла.

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

...