Почему изменяемая локальная переменная оптимизируется иначе, чем изменяемый аргумент, и почему оптимизатор генерирует цикл no-op из последнего?
Это было вдохновлено этим вопросом / ответом и последующей дискуссией в комментариях:Является ли определение «изменчивым» таким изменчивым, или 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
(Debianstable
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
и никогда не читается, что означает, что изменения к нему могут быть отменены подкак будто править. ОК, имеет смысл.
volatile
действительно для аппаратных регистров, и ясно, что локальное значение не может быть одним из них - и иначе не может быть изменено вvolatile
Кстати, если его адрес не передан ... что это не так. Слушай, просто не так много смыслаvolatile
местные ценности. Но C ++ позволяет нам объявлять их и пытается сделатьчто-то с ними. И вот, как всегда растерянные, мы спотыкаемся вперед.g()
: Какие. Перемещаяvolatile
источник в параметр передачи по значению, который все еще является еще одной локальной переменной, GCC почему-то решает, что это не так илиМеньше volatile
и поэтому ему не нужно читать его каждую итерацию ... но он все еще выполняет цикл,несмотря на то, что его тело сейчас ничего не делает.
h()
: Взяв пройденноеvolatile
как по ссылке, такое же эффективное поведение, какf()
восстанавливается, так что цикл делаетvolatile
читает.
f()
, Для разработки: представьте себеx
относится к аппаратному регистру, у которого каждое чтение имеет побочные эффекты. Вы не хотели бы пропустить ни одного из них.Добавление#define volatile /**/
приводит кmain()
будучи неоператором, как и следовало ожидать. Так, когда присутствует, даже на локальной переменнойvolatile
делает что-то ... я просто понятия не имеюкакие в случаеg()
, Что на Земле происходит там?
volatile
, Ни один адрес не выдан - и не имеютstatic
адрес, исключающий любой inline-ASMPOKE
ry - поэтому они никогда не могут быть изменены без функции. Компилятор может видеть, что каждый из них является константой, его никогда не нужно перечитывать, иvolatile
просто не правда -так (А) либопозволил быть избранным при таких ограничениях? (игра актеровкак будто они не были объявленыvolatile
) -и (B) почему только один получает право? Некоторыеvolatile
локальные переменные большеvolatile
чем другие?Оставим это несоответствие на мгновение: после того, как чтение было оптимизировано, почему компилятор все еще генерирует цикл?Это ничего не делает! Почему оптимизатор не исключает этокак будто петля не была закодирована?Это странный случай из-за порядка оптимизации анализа или что-то подобное? Поскольку этот код - глупый мысленный эксперимент, я бы не стал отчитывать GCC за это, но было бы полезно знать наверняка. (Илиg()
о цикле ручного хронометража, о котором люди мечтали все эти годы?) Если мы придем к выводу, что Стандарт не имеет отношения к какому-либо из этого, я перенесу его в Bugzilla просто для информации.
И, конечно же, более важный вопрос с практической точки зрения, хотя я не хочу, чтобы это затмевало потенциал для гиковской компиляции ... Какие, если таковые имеются, хорошо определены / правильны в соответствии со Стандартом?