Фон
Это было вдохновлено этим вопросом/ответом и последующим обсуждением в комментариях: Является ли определение "изменчивым" это изменчивым, или это GCC имеет некоторые стандартные проблемы соответствия?. Основываясь на других и моей интерпретации того, что должно происходить, как обсуждалось в комментариях, я отправил его в GCC Bugzilla: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=71793 Другое соответствующие ответы по-прежнему приветствуются.
Кроме того, этот поток с тех пор породил этот вопрос: Ли доступ к объявленному энергонезависимому объекту посредством волатильной ссылки/указателя вызывает изменчивые правила при упомянутых обращений?
Введение
Я знаю, что 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
a.k.a. 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
не объявляетсяvolatile
и никогда не читается, что означает, что изменения в нем могут быть сжаты в соответствии с правилом as-if. Хорошо, имеет смысл.- Ну, я имею в виду... любопытное. Например, не так, потому что
volatile
действительно для аппаратных регистров, и, очевидно, локальное значение не может быть одним из них - и в противном случае оно не может быть изменено способомvolatile
, если его адрес не будет удален... которого нет. Послушайте, из-за локальных значенийvolatile
не должно быть большого смысла. Но С++ позволяет нам объявить их и пытается что-то с ними сделать. И поэтому, смутившись, как всегда, мы спотыкаемся вперед.
- Ну, я имею в виду... любопытное. Например, не так, потому что
-
g()
: Что. Перемещая источникvolatile
в параметр pass-by-value, который по-прежнему является еще одной локальной переменной, GCC каким-то образом решает это или не менееvolatile
, и поэтому ему не нужно читать каждую итерацию... но он все еще выполняет цикл, несмотря на то, что его тело ничего не делает. -
h()
: Принимая пройденныйvolatile
в качестве передачи по ссылке, восстанавливается такое же эффективное поведение, какf()
, поэтому циклvolatile
читает.- Этот случай сам по себе имеет для меня практический смысл по причинам, изложенным выше, против
f()
. Чтобы уточнить: Представьте, чтоx
относится к аппаратным регистрам, из которых каждый прочитанный имеет побочные эффекты. Вы не захотите пропустить ни один из них.
- Этот случай сам по себе имеет для меня практический смысл по причинам, изложенным выше, против
Добавление #define volatile /**/
приводит к тому, что main()
является no-op, как и следовало ожидать. Итак, когда присутствует, даже на локальной переменной volatile
что-то делает... Я просто не знаю, что в случае g()
. Что там происходит на Земле?
Вопросы
- Почему локальное значение, объявленное в теле, производит разные результаты по параметру по значению, при этом первая версия чтения будет оптимизирована? Оба объявлены
volatile
. У вас также нет адреса с адресом и не имеют адресаstatic
, исключая любой встроенный ASMPOKE
ry, поэтому они никогда не могут быть изменены с помощью функции. Компилятор может видеть, что каждый из них является постоянным, его никогда не нужно перечитывать, аvolatile
просто неверно -- так (A) либо разрешено отклоняться при таких ограничениях? (действующие как если бы они не были объявлены
volatile
) - - и (B) почему только кто-то уходит? Являются ли некоторые локальные переменные
volatile
болееvolatile
чем другие?
- так (A) либо разрешено отклоняться при таких ограничениях? (действующие как если бы они не были объявлены
- Отбросить эту несогласованность на мгновение: после того, как чтение было оптимизировано, почему компилятор все еще генерирует цикл? Он ничего не делает! Почему оптимизатор не делает это так: если никакой цикл не был закодирован?
Является ли это странным угловым случаем из-за порядка оптимизации анализов или такого? Поскольку код - это глупый мысленный эксперимент, я бы не стал критиковать GCC за это, но было бы хорошо знать наверняка. (Или это g()
, о которых люди мечтали во все эти годы?) Если мы не заключим ни одного стандартного отношения ни к одному из этих случаев, я переведу его в свою Bugzilla только для их информации.
И, конечно, более важный вопрос с практической точки зрения, хотя я не хочу, чтобы это затмило потенциал для компилятора geekery... Который, если какой-либо из них, четко определен или правильный в соответствии со стандартом