Это было вдохновлено этим вопросом/ответом и последующим обсуждением в комментариях: Is the definition of “volatile” this volatile, or is GCC having some standard compliancy problems?. Основываясь на других и моей интерпретации того, что должно происходить, как обсуждалось в комментариях, я отправил его в GCC Bugzilla: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=71793 Другие соответствующие ответы по-прежнему приветствуются.Почему переменная локальная переменная оптимизирована по-разному от изменчивого аргумента и почему оптимизатор генерирует цикл no-op из последнего?
Кроме того, что нить с тех пор породила этот вопрос: Does accessing a declared non-volatile object through a volatile reference/pointer confer volatile rules upon said accesses?
Интро
Я знаю volatile
это не то, что большинство людей думают, что это и есть реализации определенного гнездо гадюк. И я, конечно, не хочу использовать приведенные ниже конструкции в любом реальном коде. Тем не менее, я полностью озадачен тем, что происходит в этих примерах, поэтому я был бы очень признателен за любое разъяснение.
Мое предположение заключается в том, что это связано с очень тонкой интерпретацией стандартных или (скорее всего?) Просто угловых шкафов для используемого оптимизатора. В любом случае, хотя и более академичный, чем практический, я надеюсь, что это будет полезно для анализа, особенно учитывая, как обычно неправильно понимается volatile
. Еще несколько точек данных - или, возможно, более вероятно, указывают на это - должно быть хорошо.
Входной
Учитывая этот код:
#include <cstddef>
void f(void *const p, std::size_t n)
{
unsigned char *y = static_cast<unsigned char *>(p);
volatile unsigned char const x = 42;
// N.B. Yeah, const is weird, but it doesn't change anything
while (n--) {
*y++ = x;
}
}
void g(void *const p, std::size_t n, volatile unsigned char const x)
{
unsigned char *y = static_cast<unsigned char *>(p);
while (n--) {
*y++ = x;
}
}
void h(void *const p, std::size_t n, volatile unsigned char const &x)
{
unsigned char *y = static_cast<unsigned char *>(p);
while (n--) {
*y++ = x;
}
}
int main(int, char **)
{
int y[1000];
f(&y, sizeof y);
volatile unsigned char const x{99};
g(&y, sizeof y, x);
h(&y, sizeof y, x);
}
Выход
g++
из gcc (Debian 4.9.2-10) 4.9.2
(Debian stable
А.К.А. Jessie) с помощью командной строки g++ -std=c++14 -O3 -S test.cpp
производит ниже ASM для main()
. Версия Debian 5.4.0-6
(текущий unstable
) производит эквивалентный код, но я только что произошло, чтобы запустить старший один первый, так вот она:
main:
.LFB3:
.cfi_startproc
# f()
movb $42, -1(%rsp)
movl $4000, %eax
.p2align 4,,10
.p2align 3
.L21:
subq $1, %rax
movzbl -1(%rsp), %edx
jne .L21
# x = 99
movb $99, -2(%rsp)
movzbl -2(%rsp), %eax
# g()
movl $4000, %eax
.p2align 4,,10
.p2align 3
.L22:
subq $1, %rax
jne .L22
# h()
movl $4000, %eax
.p2align 4,,10
.p2align 3
.L23:
subq $1, %rax
movzbl -2(%rsp), %edx
jne .L23
# return 0;
xorl %eax, %eax
ret
.cfi_endproc
Анализ
Все 3 функции встраиваются, и оба, которые выделяют volatile
местный переменные делают это в стеке по довольно очевидным причинам. Но это единственная вещь, которую они разделяют ...
f()
обеспечивает для чтения изx
на каждой итерации, предположительно из-за егоvolatile
- но просто выводит результатedx
, предположительно из-за назначенияy
ISN 't объявленоvolatile
и никогда не читается, что означает, что изменения в нем могут быть закреплены в соответствии с правилом как-если. Хорошо, имеет смысл.- Ну, я имею в виду ... вид. Например, не так, потому что
volatile
действительно для аппаратных регистров, и, очевидно, локальное значение не может быть одним из них - и в противном случае он не может быть изменен в путиvolatile
, если его адрес не будет передан ... что это не так. Послушайте, у меня не так уж много смысла, если бы изvolatile
местных значений.Но C++ позволяет нам объявить их и пытается сделать что-то с ними. И поэтому, смутившись, как всегда, мы спотыкаемся вперед.
- Ну, я имею в виду ... вид. Например, не так, потому что
g()
: Что. Перемещение источникаvolatile
в параметр pass-by-value, который по-прежнему является еще одной локальной переменной, GCC каким-то образом решает, что это не так, или меньше , и поэтому ему не нужно читать его на каждой итерации ... но он все еще выполняет цикл, , несмотря на то, что его тело теперь ничего не делает.h()
: Принимая переданноеvolatile
как пройти по ссылке, то же самое эффективное поведение, какf()
восстанавливается, так что цикл делаетvolatile
читает.- Этот случай сам по себе имеет практическое значение по причинам, изложенным выше, против
f()
. Чтобы уточнить: Представьте, чтоx
относится к регистру оборудования, из которых каждый прочитанный имеет побочные эффекты. Вы не захотите пропустить ни одного из них.
- Этот случай сам по себе имеет практическое значение по причинам, изложенным выше, против
Добавление #define volatile /**/
приводит к main()
не будучи не-оп, как и следовало ожидать. Итак, когда присутствует, даже по локальной переменной volatile
делает что-то ... Я просто не знаю что в случае g()
. Что там происходит?
Вопросы
- Почему местное значение, объявленные в теле-дают разные результаты от параметра по значению, с бывшим Устремляя чтения быть оптимизированы прочь? Оба объявлены
volatile
. У вас также нет адреса, и не имеет адресаstatic
, исключая любой inline-ASMPOKE
ry - поэтому они никогда не могут быть изменены с помощью функции. Компилятор может видеть, что каждый постоянный, никогда не должны быть перечитывать, иvolatile
просто не соответствует действительности -- так (A) либо позволило быть опущены при таких ограничениях? (действующие as-if они не были объявлены
volatile
) - - и (B) почему только один из них отклоняется? Есть некоторые
volatile
локальные переменные болееvolatile
, чем другие?
- так (A) либо позволило быть опущены при таких ограничениях? (действующие as-if они не были объявлены
- Отложить это несоответствие на мгновение: после того, как чтение было оптимизировано, почему компилятор все еще генерирует цикл? Он ничего не делает! Почему оптимизатор не достигает этого as-if не закодирован ли цикл?
Является ли это странным угловым корпусом из-за порядка оптимизации анализов или такого? Поскольку код - это глупый мысленный эксперимент, я бы не стал критиковать GCC за это, но было бы хорошо знать наверняка. (Или g()
Ручная синхронизация людей, о которых мечтали все эти годы?) Если мы заключим, что нет никакого стандартного отношения ни к одному из этого, я переведу его в их Bugzilla только для их информации.
И, конечно, более важный вопрос с практической точки зрения, хотя я не хочу, чтобы это затмило потенциал для компилятора geekery ... Который, если какой-либо из них, четко определен или правильный в соответствии с Стандартный?
TL; DR - Если это не изменяет наблюдаемое поведение программы, действительно ли это имеет значение? –
Стандарт C++ 11 (я также предполагаю, что C++ 14) говорит: «Доступ к неустойчивым объектам оценивается строго в соответствии с правилами абстрактной машины». Другими словами, правило «как-если» не применяется - вы должны строго следовать правилам абстрактной машины. Я думаю, что поведение 'g()' нарушает это; Я предполагаю, что это ошибка оптимизатора. Я также предполагаю, что большинство людей скажут, что это ошибка с низким приоритетом, и что некоторые (многие?) Не согласятся с тем, что это ошибка в первую очередь. –
@MichaelBurr Отличная цитата, не только для ее актуальности, но и для фантастического термина «абстрактная машина». Я имею дело с большим количеством тех. Я склонен думать, что это ошибка в смысле надзора, поскольку можно было бы ожидать, что 2 типа локальной переменной будут вести себя одинаково - как по моему grok, единственное заметное различие - это время, построена. Однако из-за сложности оптимизации и бесчисленных перестановок упорядочения различных этапов и т. Д. Это, очевидно, не так просто. Я бы никогда не осмелился классифицировать его как ошибку '' ittybitty'-priority, но вывод был бы замечательным. –