Почему 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?

Ответ 1

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

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

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

Фактически, здесь что-то C99 говорит об абстрактном механизме поведения в почти этих точках:

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

Пример 1:

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

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

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

7.14.1.1/5 "Функция signal

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

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

volatile sig_atomic_t running = 1;

Ответ 2

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

Ответ 3

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

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

Ответ 4

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

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

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

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

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

Ответ 5

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

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