Является ли эта избыточная оптимизация загрузки/хранения допустимой в 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 как ничто. Слишком грустно...)