Почему 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 в этом случае?


person Thomas    schedule 25.03.2010    source источник
comment
Попробуйте сделать handler статическим. Он все еще не оптимизируется?   -  person Adam Goode    schedule 25.03.2010
comment
вот что я нашел: если вы сделаете handler статическим и опустите вызов signal, цикл будет оптимизирован. Если вы либо сделаете handler нестатическим, либо поместите вызов signal, он не будет его оптимизировать. Итак, что он может подумать, так это то, что signal вызывает handler напрямую, используя переданный указатель функции (а не прерывание)?   -  person Johannes Schaub - litb    schedule 25.03.2010
comment
@litb: Отличная находка! Что может случиться, насколько известно компилятору, так это то, что signal хранит указатель на handler где-то в глобальном массиве, а затем putchar вызывает его. Вот почему компилятору не разрешено оптимизировать в этом случае.   -  person Thomas    schedule 25.03.2010
comment
@ Томас, я разочарован. Возможно, стоит сообщить GCC о встроенном значении signal, как и для других встроенных функций. Тогда он будет знать, что переданный указатель функции не вызывается из контекста обработки сигнала. Или, может быть, они явно хотят разрешить изменение энергонезависимых переменных в обработчиках сигналов, и это поведение предусмотрено?   -  person Johannes Schaub - litb    schedule 25.03.2010
comment
@litb: нет ли также проблемы в том, что повторный вызов signal вернет старый обработчик сигнала (по крайней мере, в Linux, не уверен насчет других платформ...), поэтому в принципе переданный указатель функции может использоваться каким-то другим произвольным код?   -  person Mike Dinsdale    schedule 25.03.2010
comment
@Майк, о, я вижу. Хм не думал об этом. :)   -  person Johannes Schaub - litb    schedule 26.03.2010
comment
Краткий ответ: putchar может позвонить handler.   -  person David Schwartz    schedule 05.10.2013


Ответы (5)


В общем случае компилятору сложно точно знать, к каким объектам функция может иметь доступ и, следовательно, потенциально может изменять их. В момент, когда вызывается 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;
person Michael Burr    schedule 25.03.2010
comment
Верен ли ваш последний абзац, учитывая, что handler() не является статическим (и что его адрес передается вызову signal())? Я бы предпочел, чтобы это предотвратило любую оптимизацию, основанную исключительно на содержимом этого файла c (это согласуется с тем, что litb описывает в своем комментарии к ОП) - person Mike Dinsdale; 25.03.2010
comment
@Майк Динсдейл: ты прав. Я попытался осветить это, сказав, что если это так, то здесь это не так (как упоминалось в предыдущем абзаце). Но во втором чтении это не очень ясно. Посмотрим, смогу ли я немного почистить этот абзац. - person Michael Burr; 25.03.2010
comment
Обратите внимание, что ваш анализ здесь является именно той причиной, по которой энергонезависимые статические переменные прекрасно подходят для использования с потоками, если вы используете соответствующие примитивы блокировки (например, pthread_mutex_lock). По всем тем же причинам, которые вы объяснили, компилятор по существу вынужден предположить, что pthread_mutex_lock может изменить переменную. (И в патологической реализации не совсем одновременных потоков это действительно может иметь место!) - person R.. GitHub STOP HELPING ICE; 06.02.2011
comment
@R.. компилятор по существу вынужден предположить, что pthread_mutex_lock может изменить переменную. (И в патологической реализации не совсем параллельных потоков это действительно может иметь место!) В абстрактно-формальном смысле это всегда так. - person curiousguy; 27.10.2011
comment
Поскольку running является статическим, компилятор может определить, что он недоступен извне единицы трансляции, но тогда он также не будет доступен из обработчика сигнала. Выхода нет: то, что running может быть изменено с помощью kill, означает, что оно изменяется вне ЕП. - person curiousguy; 27.10.2011

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

person R Samuel Klatchko    schedule 25.03.2010
comment
Отличное замечание о том, что handler не является статическим. Таким образом, даже если putchar определено в другой единице перевода, оно все же может косвенно изменить running таким образом. - person Johannes Schaub - litb; 25.03.2010

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

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

person Mike Mueller    schedule 25.03.2010
comment
GCC, вероятно, предполагает, что вызов putchar может изменить любую глобальную переменную Лучше не предполагать обратное, поскольку putchar действительно может делать такие вещи. - person curiousguy; 27.10.2011

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

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

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

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

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

person Thomas    schedule 25.03.2010

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

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

person curiousguy    schedule 27.10.2011