Задний план
Это было вдохновлено этим вопросом / ответом и последующим обсуждением в комментариях: Является ли определение «изменчивого» этим изменчивым, или 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
и никогда не читается, что означает изменения в нем. может быть отклонено правилом как если бы. ОК, имеет смысл.- Well, I mean... kinda. Like, not really, because
volatile
is really for hardware registers, and clearly a local value can't be one of those - and can't otherwise be modified in avolatile
way unless its address is passed out... which it's not. Look, there's just not a lot of sense to be had out ofvolatile
local values. But C++ lets us declare them and tries to do something with them. And so, confused as always, we stumble onwards.
- Well, I mean... kinda. Like, not really, because
g()
: Что. Перемещая источникvolatile
в параметр передачи по значению, который по-прежнему является еще одной локальной переменной, GCC каким-то образом решает, что это не так или меньшеvolatile
, поэтому ему не нужно читать его на каждой итерации ... но он по-прежнему выполняет цикл, несмотря на то, что его тело теперь ничего не делает.h()
: при использовании переданногоvolatile
в качестве передачи по ссылке восстанавливается то же эффективное поведение, что и дляf()
, поэтому цикл выполняетvolatile
чтения.- This case alone actually makes practical sense to me, for reasons outlined above against
f()
. To elaborate: Imaginex
refers to a hardware register, of which every read has side-effects. You wouldn't want to skip any of those.
- This case alone actually makes practical sense to me, for reasons outlined above against
Добавление #define volatile /**/
приводит к тому, что main()
не работает, как и следовало ожидать. Итак, когда он присутствует, даже в локальной переменной volatile
что-то делает ... Я просто не знаю, что в случае g()
. Что, черт возьми, там происходит?
Вопросы
- Why does a local value declared in-body produce different results from a by-value parameter, with the former letting reads be optimised away? Both are declared
volatile
. Neither have an address passed out - and don't have astatic
address, ruling out any inline-ASMPOKE
ry - so they can never be modified outwith the function. The compiler can see that each is constant, need never be re-read, andvolatile
just ain't true -- so (A) is either allowed to be elided under such constraints? (acting as-if they weren't declared
volatile
) - - и (B) почему упускается только один? Некоторые
volatile
локальные переменные большеvolatile
, чем другие?
- so (A) is either allowed to be elided under such constraints? (acting as-if they weren't declared
- Отбросим на мгновение эту несогласованность: почему после того, как чтение было оптимизировано, почему компилятор все еще генерирует цикл? Ничего не делает! Почему оптимизатор не игнорирует это как если бы цикл не был закодирован?
Это странный случай из-за порядка оптимизации анализа или чего-то подобного? Поскольку код - глупый мысленный эксперимент, я бы не стал наказывать GCC за это, но было бы хорошо знать наверняка. (Или это g()
ручной цикл синхронизации, о котором люди мечтали все эти годы?) Если мы придем к выводу, что ни на что из этого не влияет Стандарт, я перенесу его в их Bugzilla только для их информации.
И, конечно же, более важный вопрос с практической точки зрения, хотя я не хочу, чтобы это затмило потенциал компьютерных фанатиков ... Какие из них, если они есть, являются четко определенными / правильными согласно Стандарту?
g()
нарушает это; Я предполагаю, что это ошибка оптимизатора. Я также предполагаю, что большинство людей скажут, что это ошибка с низким приоритетом, и что некоторые (многие?) Не согласятся, что это ошибка в первую очередь. - person Michael Burr   schedule 07.07.2016>ittybitty
, но заключение было бы отличным. - person underscore_d   schedule 07.07.2016volatile
-объявленным объектам, «как если бы» для других все еще AOK, верно? Кажется справедливым, что посколькуy
не используется, бункеры компилятора записывают в него. Почему он по-прежнему считывается сx
, непонятно: сx
ссылкой это имеет смысл, если это аппаратный регистр, опрос которого имеет побочные эффекты. Но это не относится к случаям по стоимости ... которые, тем не менее, различаются. Кроме того: могут ли / должны ли объекты быть «сделаны» изменчивыми черезvolatile *
кажется спорным: верхний ответ на связанном Q говорит «нет», но N1381, похоже, подразумевает «да»: open-std.org/jtc1/sc22/wg14/www/docs/n1381.pdf - person underscore_d   schedule 07.07.2016g
- ошибка компилятора согласно Стандарту. - person M.M   schedule 07.07.2016g
не является ошибкой компилятора. Скажем, ассемблерный код выдал отдельные чтения, но аппаратное обеспечение ЦП объединило их, вы все равно скажете, что это ошибка компилятора? И как оптимизация более или менее заметна, если она выполняется ЦП, чем если она выполняется компилятором? Стандарт C не говорит, как должен выглядеть ассемблерный код (как это могло быть?), Он говорит, что ассемблерный код должен заставить систему делать. Любая оптимизация процессора также может быть выполнена ассемблером. Объединение операций чтения с нераспространяемыми кэшируемыми значениями - это допустимая оптимизация ЦП, поэтому оптимизация компилятора разрешена. - person David Schwartz   schedule 07.07.2016volatile
определяется реализацией) - поэтому он не будет просто выдавать ложные чтения или случайным образом комбинировать их . Мне это кажется вполне подходящим дляvolatile
, по крайней мере, для такой системы. Кроме того, ни один из этих адресов не объясняет, почемуf()
иg()
различаются или какой из двух правильный. - person underscore_d   schedule 07.07.2016x
, один раз для каждой итерации цикла. Было бы несоответствием, если бы система (будь то компилятор, или ЦП, или что-то еще) объединила все это для одного чтения. - person M.M   schedule 07.07.2016volatile
референты являются нераспространенными, кешируемыми значениями? Разве в действительности не прямо противоположное?volatile
изначально подразумевает совместное использование (обязательно: хотя и не поточно-ориентированное) в строгом смысле: программа должна перечитывать физическую ячейку памяти по запросу, если что-то еще изменило ее. Что может быть более доступным и менее кешируемым, чем это? - person underscore_d   schedule 07.07.2016volatile
была сохранена, т.е. каждая операция должна оставаться и должна происходить в ее написанном последовательность. stackoverflow.com/questions/2535148/ - person underscore_d   schedule 07.07.2016volatile
s, объявленных в стеке и не используемых совместно. - person David Schwartz   schedule 07.07.2016volatile
. ЦП может объединять чтения в этом случае , и компилятор не может остановить это. Если бы вы правильно читали стандарт, большинство современных платформ нарушали бы стандарт.) - person David Schwartz   schedule 07.07.2016i=5; i=3;
, если компилятор собирался выдать обе записи, он должен был бы сначала выдать инструкцию дляi=3;
, в противном случае окончательное значение было бы неправильным. Конечно, вы не стали бы спорить с тем, что если быi
былvolatile
, на такой платформе можно было бы сделать окончательное значение 5, потому что ЦП отменил инструкции, а не компилятор. Не может быть важен порядок инструкций. - person David Schwartz   schedule 07.07.2016volatile
s объявляется наблюдаемым поведением, и изменение наблюдаемого поведения не разрешено ни правилом «как если бы», ни каким-либо другим правилом. Я не понимаю твоего последнего предложения. Вы говорите, что компилятор не может объединить чтенияvolatile
s даже в тех случаях, когда ЦП может объединить их? Если да, то на каком основании вы пришли к такому выводу? Почему имеет значение, где происходит слияние? - person David Schwartz   schedule 08.07.2016volatile
, знает что-то, чего не знает компилятор. Есть много мест, где было бы полезно иметь, возможно, квалификаторы для параметров, чтобы явно позволить компилятору опускать квалификаторы при встраивании функции в тех случаях, когда аргументы не имеют их или имеют возвращаемые типы, соответствующие параметрам (в то время как прототипstrchr
не мог быть изменен ... - person supercat   schedule 08.07.2016volatile
, делает это по какой-то причине, а программист, заинтересованный в скорости, написание двух версий функции - одна принимает указатель с указателемvolatile
, а другая принимает указатель без указания ... - person supercat   schedule 08.07.2016volatile
, пока обработчик активен. При условии, что последний доступ к переменной перед включением обработчика - это энергозависимая запись, первый доступ после - это либо запись, либо энергозависимое чтение, и пока он включен, неквалифицированный доступ отсутствует ... - person supercat   schedule 08.07.2016volatile
обращения в другое время, когда обработчик не включен. - person supercat   schedule 08.07.2016volatile int* x = 0x10; *x; *x; int y =*x;
Компилятор не знает, имеет ли адрес чтения 0x10 побочные эффекты, поэтому он должен выполнять все чтения по порядку. ЦП знает, имеет ли чтение 0x10 побочные эффекты, или, возможно, это регистр только для чтения, и может безопасно объединяться или нет. - person mbrig   schedule 11.07.2016volatile
является явным исключением из правила «как если бы», поскольку доступ к ним определен как наблюдаемое поведение. Когда вы читаете спецификацию, в ней говорится, что вы не можете прикасаться к ним, когда это не имеет значения. (Опять же, я не согласен с тем, как вы читаете спецификацию. Но вы читаете ее, поскольку доступ к изменчивым переменным определяется как наблюдаемое поведение и поэтому не может быть изменен даже там, где это не имеет значения. Если нет , а затем объясните, что, по вашему мнению, говорится в спецификации.) - person David Schwartz   schedule 12.07.2016