Фон
Это было вдохновлено этим вопросом/ответом и последующим обсуждением в комментариях: Является ли определение "изменчивым" это изменчивым, или это 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, исключая любой встроенный ASMPOKEry, поэтому они никогда не могут быть изменены с помощью функции. Компилятор может видеть, что каждый из них является постоянным, его никогда не нужно перечитывать, аvolatileпросто неверно -- так (A) либо разрешено отклоняться при таких ограничениях? (действующие как если бы они не были объявлены
volatile) - - и (B) почему только кто-то уходит? Являются ли некоторые локальные переменные
volatileболееvolatileчем другие?
- так (A) либо разрешено отклоняться при таких ограничениях? (действующие как если бы они не были объявлены
- Отбросить эту несогласованность на мгновение: после того, как чтение было оптимизировано, почему компилятор все еще генерирует цикл? Он ничего не делает! Почему оптимизатор не делает это так: если никакой цикл не был закодирован?
Является ли это странным угловым случаем из-за порядка оптимизации анализов или такого? Поскольку код - это глупый мысленный эксперимент, я бы не стал критиковать GCC за это, но было бы хорошо знать наверняка. (Или это g(), о которых люди мечтали во все эти годы?) Если мы не заключим ни одного стандартного отношения ни к одному из этих случаев, я переведу его в свою Bugzilla только для их информации.
И, конечно, более важный вопрос с практической точки зрения, хотя я не хочу, чтобы это затмило потенциал для компилятора geekery... Который, если какой-либо из них, четко определен или правильный в соответствии со стандартом