Разрешено ли компилятору оптимизировать локальную изменчивую переменную?

79

Разрешено ли компилятору оптимизировать это (согласно стандарту 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()на оптимизированную версию. Это преобразование действительно? Если нет, какой шаг недействителен?

геза
источник
1
Комментарии не предназначены для расширенного обсуждения; этот разговор был перемещен в чат .
@philipxy: Дело не в том, «что можно производить». Речь идет о том, разрешено ли преобразование. Потому что, если это не разрешено, он не должен производить преобразованную версию.
geza
Стандарт определяет для программы последовательность обращений к летучим и другим наблюдаемым объектам, которые реализация должна учитывать. Но какой доступ к изменчивым средствам определяется реализацией. Поэтому бессмысленно спрашивать, что может произвести реализация - она ​​производит то, что определено производить. Имея некоторое описание поведения реализации, вы можете выбрать другой вариант, который вам больше нравится. Но для начала нужна одна. Может быть, вас действительно интересуют наблюдаемые правила стандарта, поскольку генерация кода не имеет значения, кроме необходимости удовлетворять правилам стандарта и реализации.
philipxy
1
@philipxy: Я поясню свой вопрос, что речь идет о стандарте. Обычно это подразумевается в таких вопросах. Мне интересно, что говорит стандарт.
geza

Ответы:

63

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

Наименьшие требования к соответствующей реализации:

  • Доступ к volatileобъектам оценивается строго по правилам абстрактной машины.

[...]

Все вместе они называются наблюдаемым поведением программы.

N3690, [intro.execution], №8

То , как именно это можно наблюдать, выходит за рамки стандарта и напрямую относится к сфере конкретной реализации, точно так же, как ввод-вывод и доступ к глобальным volatileобъектам. volatileозначает «вы думаете, что знаете все, что здесь происходит, но это не так; поверьте мне, и делайте это, не слишком умно, потому что я в вашей программе делаю свои секреты с вашими байтами». Фактически это объясняется в [dcl.type.cv] ¶7:

[Примечание: volatileэто подсказка реализации, позволяющая избежать агрессивной оптимизации, связанной с объектом, поскольку значение объекта может быть изменено средствами, не обнаруживаемыми реализацией. Кроме того, для некоторых реализаций volatile может указывать на то, что для доступа к объекту требуются специальные аппаратные инструкции. См. 1.9 для подробной семантики. В общем, семантика volatile должна быть такой же в C ++, что и в C. - конец примечания]

Маттео Италия
источник
2
Поскольку это вопрос, получивший наибольшее количество голосов, и вопрос был расширен путем редактирования, было бы неплохо отредактировать этот ответ, чтобы обсудить новые примеры оптимизации.
hyde
Правильно - «да». Этот ответ не позволяет четко отличить наблюдаемые абстрактные машины от сгенерированного кода. Последнее определяется реализацией. Например, возможно, для использования с данным отладчиком изменчивый объект гарантированно находится в памяти и / или регистре; например, как правило, при соответствующей целевой архитектуре гарантируется запись и / или чтение изменчивых объектов в специальные области памяти, указанные в прагме. Реализация определяет, как обращения отражаются в коде; он решает, как и когда объект (ы) «могут быть изменены средствами, не обнаруживаемыми реализацией». (См. Мои комментарии к вопросу.)
philipxy
12

Этот цикл можно оптимизировать с помощью правила «как если бы», потому что он не имеет наблюдаемого поведения:

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, каков ваш ответ на вопрос?
hyde
@hyde: Я прочитал вопрос: «Можно ли исключить переменную», и я отвечу «Нет». Что касается конкретной реализации x86, которая поднимает вопрос о том, можно ли поместить volatile в регистр, я не совсем уверен. Однако, даже если он будет уменьшен до xor ax, ax, этот код операции не может быть удален, даже если он выглядит бесполезным, и его нельзя объединить. В моем примере с циклом скомпилированный код должен быть выполнен xor ax, axn раз, чтобы удовлетворить правилу наблюдаемого поведения. Надеюсь, редактирование ответит на ваш вопрос.
rici
Да, вопрос был немного расширен редактированием, но, поскольку вы ответили после редактирования, я подумал, что этот ответ должен охватывать новую часть ...
hyde
2
@hyde: На самом деле, я использую volatiles таким образом в тестах, чтобы избежать оптимизации компилятором цикла, который в противном случае ничего не делает. Так что я действительно надеюсь, что я прав насчет этого: =)
rici
Стандарт говорит, что операции с volatileобъектами сами по себе являются своего рода побочным эффектом. Реализация могла бы определять их семантику таким образом, чтобы они не требовали генерации каких-либо фактических инструкций ЦП, но цикл, который обращается к объекту с изменяемым атрибутом, имеет побочные эффекты и, таким образом, не имеет права на исключение.
supercat
10

Я прошу не согласиться с мнением большинства, несмотря на полное понимание того, что volatileозначает наблюдаемый ввод-вывод.

Если у вас есть этот код:

{
    volatile int x;
    x = 0;
}

Я считаю , что компилятор может оптимизировать его прочь под как если бы правило , при условии , что:

  1. В volatileпротивном случае переменная не становится видимой извне, например, с помощью указателей (что, очевидно, не является проблемой, поскольку в данной области нет такой вещи)

  2. Компилятор не предоставляет вам механизма для внешнего доступа к этому volatile

Причина в том, что вы все равно не заметили разницы из-за критерия №2.

Однако в вашем компиляторе критерий № 2 может не выполняться ! Компилятор может попытаться предоставить вам дополнительные гарантии наблюдения за volatileпеременными «извне», например, путем анализа стека. В таких ситуациях поведение действительно является наблюдаемым, поэтому он не может быть оптимизирован прочь.

Теперь вопрос в том, отличается ли следующий код от приведенного выше?

{
    volatile int x = 0;
}

Я полагаю, что наблюдал различное поведение этого в Visual C ++ в отношении оптимизации, но я не совсем уверен, на каком основании. Может быть, инициализация не засчитывается как "доступ"? Я не уверен. Если вам интересно, это может стоить отдельного вопроса, но в остальном я считаю, что ответ такой, как я объяснил выше.

пользователь541686
источник
6

Теоретически обработчик прерывания может

  • проверьте, попадает ли обратный адрес в fn()функцию. Он может получить доступ к таблице символов или номерам строк исходного кода через инструменты или прикрепленную отладочную информацию.
  • затем измените значение x, которое будет сохранено с предсказуемым смещением от указателя стека.

… Таким образом fn()возвращая ненулевое значение.

последовал за Моникой на Кодидакт
источник
1
Или вы могли бы легче сделать это с помощью отладчика, установив точку останова в fn(). Использование volatileсоздает код, аналогичный gcc -O0этой переменной: spill / reload между каждым оператором C. ( -O0может по-прежнему объединять несколько обращений в одном операторе без нарушения согласованности отладчика, но volatileэто не разрешено.)
Питер Кордес
Или, что проще, с помощью отладчика :) Но в каком стандарте говорится, что переменная должна быть наблюдаемой? Я имею в виду, что реализация может выбрать, что она должна быть наблюдаемой. Другой может сказать, что это не наблюдается. Последний нарушает стандарт? Может быть нет. Стандарт не определяет, как локальная изменчивая переменная вообще может быть наблюдаемой.
geza
Даже, что значит «наблюдаемый»? Должен ли он быть помещен в стек? Что делать, если реестр держится x? Что, если на x86-64 xor rax, raxхранится ноль (я имею в виду регистр возвращаемого значения x), который, конечно, можно легко наблюдать / изменять с помощью отладчика (то есть, хранится информация о символах отладки, которая xхранится в rax). Это нарушает стандарт?
geza
2
−1 Любой вызов fn()может быть встроен. С MSVC 2017 и режимом выпуска по умолчанию это так. Тогда нет никакого «внутри fn()функции». В любом случае, поскольку переменная хранится автоматически, «предсказуемого смещения» нет.
Приветствия и hth. - Alf
1
0 @berendi: Да, ты прав, в этом я ошибался. Извини, плохое утро для меня в этом отношении (ошибся дважды). Тем не менее, IMO бесполезно спорить, как компилятор может поддерживать доступ через другое программное обеспечение, потому что он может делать это независимо от того volatile, и потому volatileчто не заставляет его предоставлять эту поддержку. И поэтому я убираю голос против (я был неправ), но не голосую за, потому что думаю, что эта аргументация не проясняет.
Приветствия и hth. - Alf
6

Я просто собираюсь добавить подробную ссылку на правило as-if и ключевое слово volatile . (Внизу этих страниц следуйте пунктам «См. Также» и «Ссылки», чтобы вернуться к исходным спецификациям, но я считаю, что cppreference.com намного легче читать / понимать.)

В частности, я хочу, чтобы вы прочитали этот раздел

volatile объект - объект, тип которого является изменчивым, или подобъект изменчивого объекта, или изменяемый подобъект объекта const-volatile. Каждый доступ (операция чтения или записи, вызов функции-члена и т. Д.), Выполняемый через выражение glvalue с типом с переменной volatile, рассматривается как видимый побочный эффект в целях оптимизации (то есть в рамках одного потока выполнения volatile доступы не могут быть оптимизированы или переупорядочены с другим видимым побочным эффектом, который упорядочен - до или после энергозависимого доступа. Это делает энергозависимые объекты подходящими для связи с обработчиком сигнала, но не с другим потоком выполнения, см. std :: memory_order ). Любая попытка обратиться к изменчивому объекту через энергонезависимое значение glvalue (например, через ссылку или указатель на энергонезависимый тип) приводит к неопределенному поведению.

Таким образом, ключевое слово volatile специально предназначено для отключения оптимизации компилятора для glvalues . Единственное, на что здесь может повлиять ключевое слово volatile, так это то return x, что компилятор может делать все, что захочет, с остальной частью функции.

Насколько компилятор может оптимизировать возврат, зависит от того, насколько компилятору разрешено оптимизировать доступ к x в этом случае (поскольку он ничего не переупорядочивает и, строго говоря, не удаляет возвращаемое выражение. , но он читает и записывает в стек, который должен быть в состоянии упростить.) Итак, когда я это читал, это серая область в том, насколько компилятор может оптимизировать, и это легко может быть аргументировано обоими способами.

Боковое примечание: в этих случаях всегда предполагайте, что компилятор будет делать противоположное тому, что вы хотели / требовали. Вам следует либо отключить оптимизацию (по крайней мере, для этого модуля), либо попытаться найти более определенное поведение для того, что вы хотите. (Вот почему так важно модульное тестирование). Если вы считаете, что это дефект, вы должны сообщить об этом разработчикам C ++.


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

glvalue Выражение glvalue - это либо lvalue, либо xvalue.

Свойства:

Glvalue может быть неявно преобразовано в prvalue с помощью неявного преобразования lvalue-to-rvalue, массива в указатель или функции в указатель. Значение glvalue может быть полиморфным: динамический тип идентифицируемого объекта не обязательно является статическим типом выражения. Glvalue может иметь неполный тип, если это разрешено выражением.


xvalue Следующие выражения являются выражениями xvalue:

вызов функции или перегруженное операторное выражение, возвращаемый тип которого - ссылка rvalue на объект, например std :: move (x); a [n], встроенное выражение индекса, где один операнд является массивом rvalue; am, член объектного выражения, где a - значение r, а m - нестатический элемент данных не ссылочного типа; a. * mp, указатель на член объектного выражения, где a - rvalue, а mp - указатель на член данных; а? b: c, тернарное условное выражение для некоторых b и c (подробности см. в определении); выражение приведения к rvalue, ссылка на тип объекта, например static_cast (x); любое выражение, обозначающее временный объект после временной материализации. (начиная с C ++ 17) Свойства:

То же, что и rvalue (ниже). То же, что и glvalue (ниже). В частности, как и все rvalues, xvalues ​​привязываются к ссылкам rvalue, и, как все glvalue, xvalue могут быть полиморфными, а значения x, не относящиеся к классу, могут быть cv-квалифицированными.


lvalue Следующие выражения являются выражениями lvalue:

имя переменной, функции или члена данных, независимо от типа, например std :: cin или std :: endl. Даже если тип переменной является ссылкой rvalue, выражение, состоящее из ее имени, является выражением lvalue; вызов функции или перегруженное операторное выражение, возвращаемый тип которого - ссылка lvalue, например std :: getline (std :: cin, str), std :: cout << 1, str1 = str2 или ++ it; a = b, a + = b, a% = b и все другие встроенные выражения присваивания и составные выражения присваивания; ++ a и --a, встроенные выражения пре-инкремента и пре-декремента; * p, встроенное косвенное выражение; a [n] и p [n], встроенные выражения нижнего индекса, кроме тех случаев, когда a является массивом rvalue (начиная с C ++ 11); am, член объектного выражения, за исключением случаев, когда m является перечислителем членов или нестатической функцией-членом, или где a - значение r, а m - нестатический элемент данных не ссылочного типа; p-> m, встроенный член выражения указателя, за исключением тех случаев, когда m является перечислителем членов или нестатической функцией-членом; a. * mp, указатель на член объектного выражения, где a - lvalue, а mp - указатель на член данных; p -> * mp, встроенный указатель на член выражения указателя, где mp - указатель на член данных; a, b, встроенное выражение-запятая, где b - lvalue; а? b: c, тернарное условное выражение для некоторых b и c (например, когда оба являются lvalue одного типа, но подробности см. в определении); строковый литерал, например «Hello, world!»; выражение приведения к ссылочному типу lvalue, например static_cast (x); вызов функции или перегруженное выражение оператора, чей возвращаемый тип - ссылка rvalue на функцию; выражение приведения к rvalue, ссылка на тип функции, например static_cast (x). (начиная с C ++ 11) Свойства:

То же, что и glvalue (ниже). Можно взять адрес lvalue: & ++ i 1 и & std :: endl - допустимые выражения. Изменяемое lvalue может использоваться как левый операнд встроенных операторов присваивания и составного присваивания. Lvalue может использоваться для инициализации ссылки lvalue; это связывает новое имя с объектом, идентифицированным выражением.


как если бы правило

Компилятору C ++ разрешено вносить любые изменения в программу, пока выполняется следующее:

1) В каждой точке последовательности значения всех изменчивых объектов стабильны (предыдущие оценки завершены, новые оценки не начаты) (до C ++ 11) 1) Доступ (чтение и запись) к изменчивым объектам происходит строго в соответствии с семантикой выражений, в которых они встречаются. В частности, они не переупорядочиваются по отношению к другим изменчивым доступам в том же потоке. (начиная с C ++ 11) 2) При завершении программы данные, записанные в файлы, точно такие же, как если бы программа выполнялась в том виде, в котором она была написана. 3) Текст приглашения, который отправляется на интерактивные устройства, будет показан до того, как программа ожидает ввода. 4) Если прагма ISO C #pragma STDC FENV_ACCESS поддерживается и имеет значение ON,


Если вы хотите прочитать спецификации, я считаю, что это те, которые вам нужно прочитать

Рекомендации

Стандарт C11 (ISO / IEC 9899: 2011): 6.7.3 Квалификаторы типа (стр. 121-123)

Стандарт C99 (ISO / IEC 9899: 1999): 6.7.3 Определители типа (стр: 108-110)

Стандарт C89 / C90 (ISO / IEC 9899: 1990): 3.5.3 Квалификаторы типа

Тезра
источник
Это может быть неправильно по стандарту, но любой, кто полагается на то, что в стеке что-то коснется во время выполнения, должен прекратить кодирование. Я бы сказал, что это стандартный дефект.
Meneldal
1
@meneldal: Это слишком широкое утверждение. Использование _AddressOfReturnAddressвключает, например, анализ стека. Люди анализируют стек по веским причинам, и это не обязательно потому, что сама функция полагается на него для обеспечения правильности.
user541686
1
glvalue здесь:return x;
geza
@geza Извините, это все тяжело читать. Это glvalue, потому что x - переменная? Кроме того, для «не может быть оптимизировано», означает ли это, что компилятор вообще не может оптимизировать, или что он не может оптимизировать, изменяя выражение? (Похоже, компилятору все еще разрешено оптимизировать здесь, потому что у них нет порядка доступа, который нужно поддерживать, и выражение все еще решается, только более оптимизированным способом) Я вижу, что это аргументируется в обоих направлениях без более глубокого понимания спецификации.
Tezra
Вот цитата из вашего собственного ответа :) «Следующие выражения являются выражениями lvalue: имя переменной ...»
геза
-1

Думаю, я никогда не видел, чтобы локальная переменная, использующая volatile, не была указателем на volatile. Как в:

int fn() {
    volatile int *x = (volatile int *)0xDEADBEEF;
    *x = 23;   // request data, 23 = temperature 
    return *x; // return temperature
}

В других известных мне случаях volatile используется глобальная переменная, записанная в обработчике сигнала. Никаких указателей здесь нет. Или доступ к символам, определенным в сценарии компоновщика, по конкретным адресам, относящимся к оборудованию.

Здесь гораздо легче объяснить, почему оптимизация изменила наблюдаемые эффекты. Но то же правило применяется к вашей локальной изменчивой переменной. Компилятор должен вести себя так, как будто доступ к x является наблюдаемым и не может его оптимизировать.

Госвин фон Бредерлоу
источник
3
Но это не локальная изменчивая переменная, это локальный энергонезависимый указатель на volatile int по хорошо известному адресу.
Useless
Это облегчает рассуждение о правильном поведении. Как было сказано, правила доступа к изменчивой переменной одинаковы для локальных переменных и указателей на разыменованные изменчивые переменные.
Goswin von Brederlow
Я просто обращаюсь к первому предложению вашего ответа, которое, кажется, предполагает, что xв вашем коде есть «локальная изменчивая переменная». Это не так.
Useless
Я разозлился, когда int fn (аргумент const volatile int) не компилировался.
Джошуа
4
Редактирование делает ваш ответ правильным, но просто не отвечает на вопрос. Это пример использования из учебника volatile, и он не имеет ничего общего с местным жителем. Это могло бы быть static volatile int *const x = ...в глобальном масштабе, и все, что вы говорите, было бы точно так же. Это похоже на дополнительные базовые знания, необходимые для понимания вопроса, который, я думаю, может быть не у всех, но это не настоящий ответ.
Питер Кордес