Является ли эта избыточная оптимизация загрузки/хранения допустимой в C99?

Рассмотрим следующее:

extern void bar(int *restrict);

void foo(int *restrict p) {
  int tmp;
  bar(&tmp);
  *p = tmp;
}

Позволяет ли спецификация C99 оптимизировать foo следующим образом:

extern void bar(int *restrict);

void foo(int *restrict p) {
  bar(p);
}

Я попробовал gcc, Clang и Intel Compiler в режиме -O3 и не сгенерировал код, который отражает вышеописанную оптимизацию. Это заставило меня подозревать, что эта оптимизация нарушает спецификацию. Если это не разрешено, то где это говорится в спецификации?

Примечание: мой вопрос вдохновлен этим вопросом SO

Ответ 1

Ответ - это окончательное НЕТ, это не разрешено.

Рассмотрим, что произойдет, если foo и bar взаимно рекурсивные. Например, эта реализация bar:

void bar(int *restrict p)
{
    static int q;
    if (p == &q) {
        printf("pointers match!\n");
    } else if (p == NULL) {
        foo(&q);
    }
}

bar никогда не происходит разыменований p, поэтому ограничивающий классификатор не имеет значения. Очевидно, что статическая переменная q не может иметь тот же адрес, что и автоматическая переменная tmp в foo. Поэтому foo не может передать свой параметр обратно на bar, и данная оптимизация не разрешена.

Ответ 2

Чтобы компилятор мог выполнить оптимизацию, он должен быть уверен, что независимо от того, как реализована bar, и как вызывается foo, корректное поведение не изменится.
Поскольку реализация bar и вызов foo неизвестны компилятору, когда он компилирует foo, теоретического существования такого случая достаточно, чтобы предотвратить оптимизацию, даже если этого не происходит в действительности.

Вот пример такой ситуации. Важными моментами являются: 1. Параметр p указывает на память только для записи (например, на карту памяти, подключенную к памяти).
2. bar небезопасно для использования с указателем только для записи (возможно, он пишет его, а затем читает его обратно, ожидая того же значения).
Функция foo безопасна для использования с указателем только для записи, поскольку она записывает только p. Это верно, даже если bar небезопасно, потому что bar никогда не получает p. При предлагаемой оптимизации, bar получает p, что может вызвать проблемы.

Вот пример файла, содержащего bar и вызов foo.

static int increment;

void bar(int *restrict p) {
    (*p)=0;
    if (increment) (*p)++;
}

void foo(int *restrict p);

int main(int ac, char **av) {
    int *p = get_io_addr();    /* Get a write-only memory mapped I/O address */
    increment = atoi(av[1]);
    foo(p);
    return 0;
}

Ответ 3

Краткое описание этого SO-вопроса и this wikipedia enrty предполагает, что ключевое слово ограничения может влиять только на аргументы функции. Однако, прочитав стандарт C99, в частности, раздел 6.7.3.1, дает понять, что restrict применяется ко всему контексту, в котором restrict. Таким образом, используя

void foo(int *restrict p);

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

Однако даже с этой информацией при компиляции foo компилятор понятия не имеет, что bar будет делать с передаваемой информацией. Например, рассмотрим:

void bar (unsigned long long int *p) {
    *p = ((unsigned long long int) p) % 2000;
    }

Результат зависит от значения набора указателей, а это означает, что при компиляции foo предположение оптимизации, которое вы предлагаете, не может быть окончательно сделано, поскольку результат будет другим, если предлагаемая оптимизация сделана.

Ответ 4

Оба кода НЕ эквивалентны: в первом случае функция "bar" получает указатель (и, вероятно, использует значение) "tmp", то есть локальную (и не инициализированную!) переменную. Во втором случае "bar" работает непосредственно на "p", и в целом он "найдет другое значение в" * p ". Все будет по-другому, если функция "bar" объявила свой параметр как "только выход" (например, в M $VS через макрос OUT), так как начальное значение переменной будет (должно быть) проигнорировано. (ПРИМЕЧАНИЕ: большинство версий VS фактически определяют макрос OUT как ничто. Слишком грустно...)