Разрешено ли компилятору оптимизировать это (согласно стандарту C ++ 17):
int fn() {
volatile int x = 0;
return x;
}
к этому?
int fn() {
return 0;
}
Если да, то почему? Если нет, то почему?
Вот некоторые размышления по этому поводу: текущие компиляторы компилируются fn()
как локальная переменная, помещенная в стек, а затем возвращают ее. Например, на x86-64 gcc создает это:
mov DWORD PTR [rsp-0x4],0x0 // this is x
mov eax,DWORD PTR [rsp-0x4] // eax is the return register
ret
Насколько мне известно, в стандарте не говорится, что локальная изменчивая переменная должна быть помещена в стек. Итак, эта версия была бы одинаково хороша:
mov edx,0x0 // this is x
mov eax,edx // eax is the return
ret
Здесь edx
магазины x
. Но зачем останавливаться здесь? Поскольку edx
и eax
оба равны нулю, мы могли бы просто сказать:
xor eax,eax // eax is the return, and x as well
ret
И мы перешли fn()
на оптимизированную версию. Это преобразование действительно? Если нет, какой шаг недействителен?
Ответы:
Нет. Доступ к
volatile
объектам считается наблюдаемым поведением точно так же, как ввод-вывод, без особого различия между локальными и глобальными переменными.N3690, [intro.execution], №8
То , как именно это можно наблюдать, выходит за рамки стандарта и напрямую относится к сфере конкретной реализации, точно так же, как ввод-вывод и доступ к глобальным
volatile
объектам.volatile
означает «вы думаете, что знаете все, что здесь происходит, но это не так; поверьте мне, и делайте это, не слишком умно, потому что я в вашей программе делаю свои секреты с вашими байтами». Фактически это объясняется в [dcl.type.cv] ¶7:источник
Этот цикл можно оптимизировать с помощью правила «как если бы», потому что он не имеет наблюдаемого поведения:
for (unsigned i = 0; i < n; ++i) { bool looped = true; }
Этот не может:
for (unsigned i = 0; i < n; ++i) { volatile bool looped = true; }
Второй цикл что-то делает на каждой итерации, что означает, что цикл занимает O (n) времени. Я понятия не имею, что такое константа, но я могу ее измерить, а затем у меня есть способ зацикливаться в течение (более или менее) известного количества времени.
Я могу это сделать, потому что стандарт говорит, что доступ к летучим веществам должен происходить по порядку. Если бы компилятор решил, что в данном случае стандарт неприменим, я думаю, что имел бы право подать отчет об ошибке.
Если компилятор выбирает
looped
регистр, я полагаю, у меня нет веских аргументов против этого. Но он по-прежнему должен устанавливать значение этого регистра в 1 для каждой итерации цикла.источник
xor ax, ax
(гдеax
считаетсяvolatile x
) версия вопроса действительна или недействительна? IOW, каков ваш ответ на вопрос?xor ax, ax
, этот код операции не может быть удален, даже если он выглядит бесполезным, и его нельзя объединить. В моем примере с циклом скомпилированный код должен быть выполненxor ax, ax
n раз, чтобы удовлетворить правилу наблюдаемого поведения. Надеюсь, редактирование ответит на ваш вопрос.volatile
объектами сами по себе являются своего рода побочным эффектом. Реализация могла бы определять их семантику таким образом, чтобы они не требовали генерации каких-либо фактических инструкций ЦП, но цикл, который обращается к объекту с изменяемым атрибутом, имеет побочные эффекты и, таким образом, не имеет права на исключение.Я прошу не согласиться с мнением большинства, несмотря на полное понимание того, что
volatile
означает наблюдаемый ввод-вывод.Если у вас есть этот код:
{ volatile int x; x = 0; }
Я считаю , что компилятор может оптимизировать его прочь под как если бы правило , при условии , что:
В
volatile
противном случае переменная не становится видимой извне, например, с помощью указателей (что, очевидно, не является проблемой, поскольку в данной области нет такой вещи)Компилятор не предоставляет вам механизма для внешнего доступа к этому
volatile
Причина в том, что вы все равно не заметили разницы из-за критерия №2.
Однако в вашем компиляторе критерий № 2 может не выполняться ! Компилятор может попытаться предоставить вам дополнительные гарантии наблюдения за
volatile
переменными «извне», например, путем анализа стека. В таких ситуациях поведение действительно является наблюдаемым, поэтому он не может быть оптимизирован прочь.Теперь вопрос в том, отличается ли следующий код от приведенного выше?
{ volatile int x = 0; }
Я полагаю, что наблюдал различное поведение этого в Visual C ++ в отношении оптимизации, но я не совсем уверен, на каком основании. Может быть, инициализация не засчитывается как "доступ"? Я не уверен. Если вам интересно, это может стоить отдельного вопроса, но в остальном я считаю, что ответ такой, как я объяснил выше.
источник
Теоретически обработчик прерывания может
fn()
функцию. Он может получить доступ к таблице символов или номерам строк исходного кода через инструменты или прикрепленную отладочную информацию.x
, которое будет сохранено с предсказуемым смещением от указателя стека.… Таким образом
fn()
возвращая ненулевое значение.источник
fn()
. Использованиеvolatile
создает код, аналогичныйgcc -O0
этой переменной: spill / reload между каждым оператором C. (-O0
может по-прежнему объединять несколько обращений в одном операторе без нарушения согласованности отладчика, ноvolatile
это не разрешено.)x
? Что, если на x86-64xor rax, rax
хранится ноль (я имею в виду регистр возвращаемого значенияx
), который, конечно, можно легко наблюдать / изменять с помощью отладчика (то есть, хранится информация о символах отладки, котораяx
хранится вrax
). Это нарушает стандарт?fn()
может быть встроен. С MSVC 2017 и режимом выпуска по умолчанию это так. Тогда нет никакого «внутриfn()
функции». В любом случае, поскольку переменная хранится автоматически, «предсказуемого смещения» нет.volatile
, и потомуvolatile
что не заставляет его предоставлять эту поддержку. И поэтому я убираю голос против (я был неправ), но не голосую за, потому что думаю, что эта аргументация не проясняет.Я просто собираюсь добавить подробную ссылку на правило as-if и ключевое слово volatile . (Внизу этих страниц следуйте пунктам «См. Также» и «Ссылки», чтобы вернуться к исходным спецификациям, но я считаю, что cppreference.com намного легче читать / понимать.)
В частности, я хочу, чтобы вы прочитали этот раздел
Таким образом, ключевое слово volatile специально предназначено для отключения оптимизации компилятора для glvalues . Единственное, на что здесь может повлиять ключевое слово volatile, так это то
return x
, что компилятор может делать все, что захочет, с остальной частью функции.Насколько компилятор может оптимизировать возврат, зависит от того, насколько компилятору разрешено оптимизировать доступ к x в этом случае (поскольку он ничего не переупорядочивает и, строго говоря, не удаляет возвращаемое выражение. , но он читает и записывает в стек, который должен быть в состоянии упростить.) Итак, когда я это читал, это серая область в том, насколько компилятор может оптимизировать, и это легко может быть аргументировано обоими способами.
Боковое примечание: в этих случаях всегда предполагайте, что компилятор будет делать противоположное тому, что вы хотели / требовали. Вам следует либо отключить оптимизацию (по крайней мере, для этого модуля), либо попытаться найти более определенное поведение для того, что вы хотите. (Вот почему так важно модульное тестирование). Если вы считаете, что это дефект, вы должны сообщить об этом разработчикам C ++.
Все это все еще очень трудно читать, поэтому постарайтесь включить то, что я считаю актуальным, чтобы вы могли прочитать это сами.
как если бы правило
Если вы хотите прочитать спецификации, я считаю, что это те, которые вам нужно прочитать
источник
_AddressOfReturnAddress
включает, например, анализ стека. Люди анализируют стек по веским причинам, и это не обязательно потому, что сама функция полагается на него для обеспечения правильности.return x;
Думаю, я никогда не видел, чтобы локальная переменная, использующая volatile, не была указателем на volatile. Как в:
int fn() { volatile int *x = (volatile int *)0xDEADBEEF; *x = 23; // request data, 23 = temperature return *x; // return temperature }
В других известных мне случаях volatile используется глобальная переменная, записанная в обработчике сигнала. Никаких указателей здесь нет. Или доступ к символам, определенным в сценарии компоновщика, по конкретным адресам, относящимся к оборудованию.
Здесь гораздо легче объяснить, почему оптимизация изменила наблюдаемые эффекты. Но то же правило применяется к вашей локальной изменчивой переменной. Компилятор должен вести себя так, как будто доступ к x является наблюдаемым и не может его оптимизировать.
источник
x
в вашем коде есть «локальная изменчивая переменная». Это не так.volatile
, и он не имеет ничего общего с местным жителем. Это могло бы бытьstatic volatile int *const x = ...
в глобальном масштабе, и все, что вы говорите, было бы точно так же. Это похоже на дополнительные базовые знания, необходимые для понимания вопроса, который, я думаю, может быть не у всех, но это не настоящий ответ.