C / C ++ volatile имеет очень узкий диапазон гарантированных применений: для непосредственного взаимодействия с внешним миром (обработчики сигналов, написанные на C / C ++, находятся "снаружи", когда они вызываются асинхронно);Вот почему доступ к изменчивым объектам определяется как наблюдаемые , так же как консольный ввод-вывод и выходное значение программы (возвращаемое значение main).
Один из способов увидеть этопредставьте себе, что любой изменчивый доступ фактически транслируется с помощью ввода-вывода на специальной консоли или терминале или паре устройств FIFO с именами Доступы и Значения , где:
- энергозависимая запись
x = v;
к объекту x типа T преобразуется в запись в FIFO Доступ порядок записи, указанный как 4-uplet ("write", T, &x, v)
- энергозависимое чтение (lvalueпреобразование в rvalue)
x
переводится в запись в Доступы 3-uplet ("read", T, &x)
и ожидание значения в Значения .
Таким образом, volatile точно так же, как интерактивная консоль.
Хорошая спецификация volatile - это семантика ptrace (которую никто, кроме меня, не использует, но это все же самая хорошая спецификация volatile):
- переменная может быть exaдобывается отладчиком / ptrace после того, как программа была остановлена в четко определенной точке ;
- доступ к любому энергозависимому объекту представляет собой набор четко определенных точек ПК (счетчик программ), так что можно установить точку останова (**): выражение, выполняющее энергозависимый доступ, преобразуется в набор адресов в кодегде разрыв вызывает разрыв в определенном выражении C / C ++;
- состояние любого изменчивого объекта может быть произвольно изменено (*) с помощью ptrace при остановке программы, ограничено только допустимыми значениямиобъект в C / C ++;изменение битового шаблона энергозависимого объекта с помощью ptrace эквивалентно добавлению выражения присваивания в C / C ++ в четко определенную точку останова C / C ++, поэтому это эквивалентно изменению исходного кода C / C ++ во время выполнения.
Это означает, что у вас есть хорошо определенное наблюдаемое состояние ptrace энергозависимых объектов в этих точках, точка.
(*) Но вы не можете установить для энергозависимого объекта недопустимый битовый шаблон с помощью ptrace:компилятор может предположить, что любой объект имеет допустимую битовую комбинацию , как определено ABI .Любое использование ptrace для доступа к изменчивому состоянию должно соответствовать спецификации ABI объектов, совместно используемых с отдельно скомпилированным кодом.Например, компилятор может предположить, что у изменчивого числового объекта нет отрицательного нулевого значения, если ABI не позволяет это.(Очевидно, отрицательный ноль является допустимым состоянием, семантически отличным от положительного нуля, для чисел с плавающей запятой IEEE.)
(**) Встраивание и развертывание цикла могут генерировать много точек в ассемблере / двоичном коде, соответствующих уникальному C / C ++точка;отладчики справляются с этим, устанавливая множество точек останова уровня ПК для одной точки останова уровня источника.
Семантика ptrace даже не подразумевает, что изменяемая локальная переменная хранится в стеке, а не в регистре;это означает, что расположение переменной, как описано в данных отладки, может быть изменено либо в адресуемой памяти посредством ее стабильного адреса в стеке (очевидно, стабильного в течение всего времени вызова функции), либо в представлении сохраненных регистровприостановленная программа, которая находится во временной полной копии регистров, сохраненных планировщиком, когда поток выполнения приостановлен.
[На практике все компиляторы предоставляют более строгую гарантию, чем семантика ptrace: все изменчивые объекты имеютстабильный адрес, даже если его адрес никогда не берется в коде C / C ++;эта гарантия иногда бесполезна и строго пессимистична.Легкая семантическая гарантия ptrace сама по себе чрезвычайно полезна для автоматической переменной в регистре в «сборке высокого уровня».]
Вы не можете проверить работающую программу (или поток), не остановив ее;вы не можете наблюдать с любого процессора без синхронизации (ptrace обеспечивает такую синхронизацию).
Эти гарантии сохраняются на любом уровне оптимизации.При минимальной оптимизации все переменные фактически практически изменчивы, и программу можно остановить при любом выражении.
При более высоком уровне оптимизации вычисления сокращаются, и переменные могут даже оптимизироваться, если они не содержат полезной информации для какого-либозаконный прогон;наиболее очевидным случаем является переменная «квазиконст», которая не объявлена как константная, а использует a-if const: устанавливается один раз и никогда не изменяется.Такая переменная не несет никакой информации во время выполнения, если выражение, которое использовалось для ее установки, может быть пересчитано позже.
Многие переменные, которые содержат полезную информацию, все еще имеют ограниченный диапазон: если в программе нет выражения, которое может установитьцелочисленный тип со знаком и отрицательный математический результат (результат, который действительно отрицательный, а не отрицательный из-за переполнения в системе с 2 дополнениями), компилятор может предположить, что они не имеют отрицательных значений.Любая попытка установить для них отрицательное значение в отладчике или через ptrace будет не поддерживаться, так как компилятор может генерировать код, который интегрирует предположение;превращение объекта в энергозависимый заставило бы компилятор разрешить любое возможное допустимое значение для объекта, даже если в полном коде присутствуют только назначения положительных значений (код во всех путях, которые могут получить доступ к этому объекту, в каждом TU (единице перевода)который может получить доступ к объекту).
Обратите внимание, что для любого объекта, который совместно используется за пределами набора коллективно переведенного кода (все TU, которые скомпилированы и оптимизированы вместе), ничего о возможных значениях нетобъекта можно предположить рядом с применимым ABI.
Ловушка (а не ловушка, как в вычислениях) заключается в том, чтобы ожидать, что Java изменчива как семантика, по крайней мере, в одном ЦП, в линейном, упорядоченном семантическом программировании (где естьопределение no внеочередного исполнения, так как в состоянии только POV для состояния, единственного и единственного процессора):
int *volatile p = 0;
p = new int(1);
Нет никакой устойчивой гарантии, что p
может быть только нулевым или указывать на объектсо значением 1: нет никакого изменчивого упорядочения, подразумеваемого между инициализацией int
и настройку энергозависимого объекта, поэтому обработчик асинхронного сигнала или точка останова в энергозависимом присвоении могут не видеть инициализированный int
.
Но энергозависимый указатель не может изменяться умозрительно: до тех пор, пока компиляторполучает гарантию, что выражение rhs (правая сторона) не будет генерировать исключение (таким образом, оставляя p
нетронутым), оно не может изменять изменяемый объект (так как изменяемый доступ является наблюдаемым по определению).
Возвращаясь к вашему коду:
INTENABLE = 0; // volatile write (A)
my_var += 5; // normal write
INTENABLE = 1; // volatile write (B)
Здесь INTENABLE
является изменчивым, поэтому все доступы наблюдаемы;компилятор должен производить именно эти побочные эффекты;обычные записи являются внутренними для абстрактной машины, и компилятору нужно только сохранить эти побочные эффекты WRT для получения правильного результата, без учета каких-либо сигналов, которые находятся за пределами абстрактной семантики C / C ++.
В терминахиз семантики ptrace вы можете установить точку останова в точках (A) и (B) и наблюдать или изменить значение INTENABLE
, но это все.Хотя my_var
не может быть полностью оптимизирован, так как он доступен внешним кодом (кодом передачи сигнала), но в этой функции нет ничего другого, что может получить к нему доступ, , поэтому конкретное представление my_var
не имеетчтобы соответствовать его значению согласно абстрактной машине в этой точке .
Это отличается, если у вас есть вызов к истинно внешнему (не анализируемому компилятором, вне "коллективно"переведенный код ") промежуточная функция:
INTENABLE = 0; // volatile write (A)
external_func_1(); // actual NOP be can access my_var
my_var += 5; // normal write
external_func_2(); // actual NOP be can access my_var
INTENABLE = 1; // volatile write (B)
Обратите внимание, что необходимы оба этих внешних вызова функций" ничего не делать, возможно, делать что-либо ":
external_func_1()
возможно наблюдает предыдущее значение my_var
external_func_2()
возможно наблюдает новое значение my_var
Эти вызовы относятся к внешним, отдельно скомпилированным функциям NOP, которые должны выполняться в соответствии с ABI;таким образом * все глобально доступные объекты должны нести представление ABI их значения абстрактной машины : объекты должны достичь своего канонического состояния, в отличие от оптимизированного состояния, когда оптимизатор знает, что некоторое конкретное представление памяти некоторых объектов не достиглозначение абстрактной машины.
В GCC такая внешняя функция без действия может быть записана либо asm("" : : : "memory");
, либо просто asm("");
."memory"
указан неопределенно, но ясно означает «доступ к чему-либо в памяти, чей адрес был пропущен глобально».
[Смотрите здесь, я полагаюсь на прозрачное намерение спецификации, а не на ее слова, так как слова очень часто неправильно выбираются (#) и никем не используются для построения реализации в любом случае, и толькомнение людей считается, слова никогда не имеют значения.
(#) по крайней мере в мире распространенных языков программирования, где люди не имеют квалификации для написания формальных или даже правильных спецификаций.]