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

Разрешено ли компилятору оптимизировать это (согласно стандарту С++ 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

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

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

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

[...]

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

N3690, [intro.execution], ¶8

То, как именно это наблюдается, выходит за рамки стандарта и попадает прямо в область реализации, точно так же, как I/O и доступ к глобальным volatile объектам. volatile означает "Вы думаете, что знаете все, что здесь происходит, но это не так, поверьте мне, и сделать этот материал, не будучи слишком умным, потому что я в вашей программе делать мой секретный материал с байтами". Это фактически объясняется в [dcl.type.cv] ¶7:

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

Ответ 2

Этот цикл можно оптимизировать с помощью правила as-if, поскольку он не имеет наблюдаемого поведения:

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 для каждой итерации цикла.

Ответ 3

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

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

{
    volatile int x;
    x = 0;
}

Я считаю, что компилятор может оптимизировать его в соответствии с правилом as-if, предполагая, что:

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

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

Обоснование заключается в том, что вы все равно не могли наблюдать разницу из-за критерия № 2.

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

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

{
    volatile int x = 0;
}

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

Ответ 4

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

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

... таким образом, fn() возвращает ненулевое значение.

Ответ 5

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

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

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

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

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

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


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

glvalue Выражение glvalue имеет значение lvalue или xvalue.

Свойства:

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


xvalue Следующие выражения представляют собой выражения xvalue:

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

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


lvalue Следующие выражения представляют собой выражения lvalue:

имя переменной, функцию или элемент данных, независимо от типа, например std :: cin или std :: endl. Даже если тип переменной является ссылкой rvalue, выражение, состоящее из его имени, является выражением lvalue; вызов функции или перегруженное операторное выражение, возвращаемым типом которого является lvalue reference, например 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 является rvalue, а m является нестационарным членом данных не ссылочного типа; p-> m, встроенный член выражения указателя, за исключением тех случаев, когда m является членом-членом или нестатической функцией-членом; a. * mp, указатель на элемент выражения объекта, где a - значение l, а mp - указатель на элемент данных; p-> * mp, встроенный указатель на элемент выражения указателя, где mp - указатель на элемент данных; a, b, встроенное выражение для запятой, где b - значение l; a? b: c, тернарное условное выражение для некоторых b и c (например, когда оба являются lvalues одного и того же типа, но см. определение для деталей); строковый литерал, такой как "Hello, world!"; выражение cast для ссылочного типа lvalue, например static_cast (x); вызов функции или перегруженное операторное выражение, тип возвращаемого значения которого - значение 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 #pragma STDC FENV_ACCESS и установлено значение ON, изменения в среде с плавающей запятой (исключения с плавающей запятой и режимы округления) гарантируются наблюдаемыми арифметическими операторами с плавающей запятой и функцией вызовы, как если бы они выполнялись как написанные, за исключением того, что результат любого выражения с плавающей запятой, отличного от приведения и присвоения, может иметь диапазон и точность типа с плавающей запятой, отличного от типа выражения (см. FLT_EVAL_METHOD), несмотря на вышеизложенное, промежуточные результаты любого выражения с плавающей запятой может быть рассчитан как бесконечный диапазон и точность (если только #pragma STDC FP_CONTRACT не выключен)


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

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

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

Стандарт C99 (ISO/IEC 9899: 1999): 6.7.3 Типовые классификаторы (p: 108-110)

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

Ответ 6

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

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

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

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