Этот вопрос является главным образом академическим. Я спрашиваю из любопытства, не потому что это создает фактическую проблему для меня.
Рассмотрите следующую неправильную программу 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
в этом случае?
В общем случае компилятору сложно точно знать, к каким объектам функция может иметь доступ и, следовательно, потенциально может изменять. В момент вызова 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;
GCC, вероятно, предполагает, что вызов putchar
может изменить любую глобальную переменную, включая работающую
.
Обратите внимание на атрибут функции pure , который указывает, что функция не имеет побочных эффектов для глобального состояния. Я подозреваю, что если вы замените putchar () вызовом «чистой» функции, GCC повторно введет оптимизацию цикла.
Спасибо всем за ваши ответы и комментарии. Они были очень полезны, но ни один из них не дает полной картины. [Edit: Ответ Майкла Бурра теперь дает, делая этот ответ несколько излишним]. Я подведу итог здесь.
Хотя running
статичен, handler
не статичен; поэтому он может быть вызван из putchar
и таким образом изменить running
. Поскольку реализация putchar
на данный момент не известна, можно предположить, что она может вызвать handler
из тела цикла while
.
Предположим, что обработчик
был бы статическим. Можем ли мы тогда оптимизировать проверку выполнения
? Ответ - нет, потому что реализация signal
также находится вне этой единицы компиляции. Насколько известно gcc, signal
может где-то хранить адрес handle
(что, собственно, и происходит), а putchar
может затем вызвать handler
через этот указатель, даже если у него нет прямого доступа к этой функции.
Итак, в каких случаях можно оптимизировать проверку выполнения
? Похоже, что это возможно только в том случае, если тело цикла не вызывает никаких функций извне этой единицы трансляции, так что во время компиляции известно, что происходит и не происходит внутри тела цикла.
Это объясняет, почему забывание volatile
на практике не является такой большой проблемой, как может показаться на первый взгляд.
Поскольку вызов putchar ()
может изменить значение запущенного
(GCC знает только, что putchar ()
- это внешняя функция, и она не знает, что она делает - поскольку GCC знает, что putchar ()
может вызвать handler ()
).