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