После глядя на кучу из других вопросов и их ответов , я получаю впечатление , что не существует никакого широко распространенного соглашения о том , что «летучий» ключевое слово в C означает точно.
Даже сам стандарт не достаточно ясен для того, чтобы все могли понять, что это значит .
Среди других проблем:
- Кажется, он дает разные гарантии в зависимости от вашего оборудования и от вашего компилятора.
- Это влияет на оптимизацию компилятора, но не на аппаратную оптимизацию, поэтому на усовершенствованном процессоре, который выполняет свои собственные оптимизации во время выполнения, даже не ясно, может ли компилятор предотвратить любую оптимизацию, которую вы хотите предотвратить. (Некоторые компиляторы генерируют инструкции для предотвращения некоторых аппаратных оптимизаций в некоторых системах, но это никак не стандартизировано.)
Чтобы подвести итог проблемы, кажется (после прочтения много), что «volatile» гарантирует что-то вроде: Значение будет прочитано / записано не только из / в регистр, но по крайней мере в кэш L1 ядра, в том же порядке, что чтения / записи появляются в коде. Но это кажется бесполезным, поскольку чтение / запись из / в регистр уже достаточны в том же потоке, в то время как координация с кешем L1 не гарантирует что-либо еще в отношении координации с другими потоками. Я не могу себе представить, когда это может быть важно синхронизировать только с кешем L1.
ИСПОЛЬЗОВАНИЕ 1
Единственное широко согласованное использование volatile, по-видимому, относится к старым или встроенным системам, где определенные области памяти аппаратно отображаются на функции ввода / вывода, например, бит в памяти, который управляет (напрямую, в аппаратном обеспечении) индикатором. или бит в памяти, который сообщает вам, нажата ли клавиша на клавиатуре или нет (потому что она подключена аппаратно непосредственно к клавише).
Кажется, что «use 1» не встречается в переносимом коде, цели которого включают многоядерные системы.
ИСПОЛЬЗОВАНИЕ 2
Не слишком отличается от «использования 1» память, которая может быть прочитана или записана в любое время обработчиком прерываний (который может управлять подсветкой или сохранять информацию с ключа). Но уже для этого у нас есть проблема, заключающаяся в том, что в зависимости от системы обработчик прерываний может работать на другом ядре со своим собственным кешем памяти , а «volatile» не гарантирует когерентность кеша во всех системах.
Таким образом, «использование 2», по-видимому, выходит за рамки того, что может дать «изменчивый».
ИСПОЛЬЗОВАНИЕ 3
Единственные бесспорное использование я вижу , чтобы предотвратить неправильную оптимизацию доступов с помощью различных переменных , указывающих на ту же области памяти , что компилятор не понимает та же память. Но это возможно только неоспоримо , потому что люди не говорят об этом - я видел только одно упоминание о нем. И я думал, что стандарт C уже признал, что «разные» указатели (например, разные аргументы для функции) могут указывать на один и тот же элемент или близлежащие элементы, и уже указывал, что компилятор должен создавать код, который работает даже в таких случаях. Однако я не смог быстро найти эту тему в последнем (500 страниц!) Стандарте.
Таким образом, «использовать 3», возможно, не существует вообще?
Отсюда мой вопрос:
Гарантирует ли «volatile» что-либо вообще в переносимом коде C для многоядерных систем?
РЕДАКТИРОВАТЬ - обновить
Просматривая последний стандарт , похоже, что ответ, по крайней мере, очень ограничен: да.
1. Стандарт многократно определяет специальную обработку для конкретного типа "volatile sig_atomic_t". Однако в стандарте также говорится, что использование функции сигнала в многопоточной программе приводит к неопределенному поведению. Таким образом, этот вариант использования кажется ограниченным связью между однопоточной программой и ее обработчиком сигналов.
2. Стандарт также устанавливает четкое значение для «volatile» по отношению к setjmp / longjmp. (Пример кода, где это важно, дан в других вопросах и ответах .)
Таким образом, возникает более точный вопрос:
гарантирует ли «volatile» что-либо вообще в переносимом коде C для многоядерных систем, кроме (1) разрешения однопоточной программе получать информацию от своего обработчика сигналов или (2) разрешения setjmp код для просмотра переменных, измененных между setjmp и longjmp?
Это все еще вопрос да / нет.
Если «да», было бы здорово, если бы вы могли показать пример безошибочного переносимого кода, который становится ошибочным, если опущен «volatile». Если «нет», то я предполагаю, что компилятор может игнорировать «volatile» вне этих двух очень специфических случаев для многоядерных целей.
volatile
информирования программы о том, что она может изменяться асинхронно.volatile
именно того, что, я считаю, необходимо.Ответы:
Нет, это абсолютно не так . И это делает volatile практически бесполезным для безопасного кода MT.
Если бы это было так, то volatile было бы неплохо для переменных, совместно используемых несколькими потоками, так как упорядочение событий в кеше L1 - это все, что вам нужно сделать в типичном ЦП (то есть многоядерном или многопроцессорном на материнской плате), способном взаимодействовать. таким образом, что нормальная реализация многопоточности C / C ++ или Java возможна с типичными ожидаемыми затратами (то есть, не большими затратами на большинство атомарных или неконтролируемых операций мьютекса).
Но volatile не обеспечивает какого-либо гарантированного упорядочения (или «видимости памяти») в кэше ни в теории, ни на практике.
(Примечание: следующее основано на правильной интерпретации стандартных документов, намерениях стандарта, исторической практике и глубоком понимании ожиданий авторов компиляторов. Этот подход основан на истории, реальных практиках, а также ожиданиях и понимании реальных людей в реальный мир, который намного сильнее и надежнее, чем синтаксический анализ слов документа, который, как известно, не является звездной спецификацией и который много раз пересматривался.)
На практике volatile гарантирует ptrace-способность, то есть способность использовать отладочную информацию для работающей программы на любом уровне оптимизации , а также тот факт, что отладочная информация имеет смысл для этих энергозависимых объектов:
ptrace
(механизм, подобный ptrace) для установки значимых точек останова в точках последовательности после операций, связанных с изменчивыми объектами: вы действительно можете разрывать именно в этих точках (обратите внимание, что это работает, только если вы хотите установить много точек останова, как и любой другой). Оператор C / C ++ может быть скомпилирован во множество различных начальных и конечных точек сборки (как в массово развернутом цикле);Volatile гарантирует на практике немного больше, чем строгая интерпретация ptrace: он также гарантирует, что volatile автоматические переменные имеют адрес в стеке, так как они не назначаются регистру, распределение регистров, которое сделало бы манипуляции ptrace более деликатными (компилятор может выводить отладочную информацию, чтобы объяснить, как переменные распределяются по регистрам, но чтение и изменение состояния регистров немного более сложны, чем доступ к адресам памяти).
Обратите внимание, что полная возможность отладки программы, которая учитывает все переменные, изменчивые, по крайней мере, в точках последовательности, обеспечивается режимом «нулевой оптимизации» компилятора, режимом, который все еще выполняет тривиальные оптимизации, такие как арифметические упрощения (обычно нет гарантированного оптимизация во всех режимах). Но volatile сильнее, чем неоптимизация:
x-x
его можно упростить для целого,x
но не для volatile объекта.Таким образом, volatile означает гарантированную компиляцию как есть , как перевод из исходного кода в двоичный / сборка компилятором системного вызова не является реинтерпретацией, изменением или оптимизацией каким-либо образом компилятором. Обратите внимание, что библиотечные вызовы могут быть или не быть системными вызовами. Многие официальные системные функции на самом деле являются библиотечными функциями, которые предлагают тонкий слой вставки и обычно откладываются до ядра в конце. (В частности
getpid
, не нужно переходить к ядру, и он вполне может прочитать область памяти, предоставленную ОС, содержащую информацию.)Изменчивые взаимодействия - это взаимодействия с внешним миром реальной машины , которые должны следовать «абстрактной машине». Они не являются внутренним взаимодействием частей программы с другими частями программы. Компилятор может только рассуждать о том, что он знает, то есть о внутренних частях программы.
Генерация кода для энергозависимого доступа должна следовать за наиболее естественным взаимодействием с этой областью памяти: это не должно вызывать удивления. Это означает, что некоторые энергозависимые обращения должны быть атомарными : если естественный способ чтения или записи представления a
long
в архитектуре является атомарным, то ожидается, что чтение или запись avolatile long
будет атомарным, поскольку компилятор не должен генерировать глупый неэффективный код для доступа к изменяемым объектам, например, побайтово .Вы должны быть в состоянии определить это, зная архитектуру. Вам не нужно ничего знать о компиляторе, так как volatile означает, что компилятор должен быть прозрачным .
Но volatile делает не более чем принудительную передачу ожидаемой сборки для наименее оптимизированных для конкретных случаев операций с памятью: volatile семантика означает общую семантику падежа.
Общий случай - то, что делает компилятор, когда у него нет никакой информации о конструкции: f.ex. Вызов виртуальной функции для lvalue через динамическую диспетчеризацию является общим случаем, а прямой вызов переопределителя после определения во время компиляции типа объекта, обозначенного выражением, является частным случаем. У компилятора всегда есть общая обработка всех конструкций, и он следует ABI.
Volatile не делает ничего особенного для синхронизации потоков или обеспечения «видимости памяти»: volatile предоставляет гарантии только на абстрактном уровне, видимом из потока, выполняющего или остановившего поток, то есть внутри ядра ЦП :
Только второй пункт означает, что volatile бесполезен в большинстве проблем связи между потоками; первый пункт по существу не имеет отношения к любой программной проблеме, которая не включает в себя связь с аппаратными компонентами вне ЦП, но все же на шине памяти.
Свойство volatile, обеспечивающее гарантированное поведение с точки зрения ядра, выполняющего поток, означает, что асинхронные сигналы, доставляемые этому потоку, которые запускаются с точки зрения порядка выполнения этого потока, см. В операциях в порядке исходного кода. ,
Если вы не планируете отправлять сигналы своим потокам (чрезвычайно полезный подход к консолидации информации о текущих запущенных потоках без предварительно согласованной точки остановки), volatile не для вас.
источник
Я не эксперт, но у cppreference.com есть то, что мне кажется довольно хорошей информацией
volatile
. Вот суть этого:Это также дает некоторое использование:
И, конечно, упоминается, что
volatile
это не полезно для синхронизации потоков:источник
longjmp
в коде C ++.Во-первых, исторически существовали различные отклонения в отношении разных толкований значения
volatile
доступа и тому подобное. См. Это исследование: Летучие вещества некомпилированы, и что с этим делать .Помимо различных проблем, упомянутых в этом исследовании, поведение
volatile
является переносимым, за исключением одного из них: когда они действуют как барьеры памяти . Барьер памяти - это некоторый механизм, который предотвращает одновременное неупорядоченное выполнение вашего кода. Использованиеvolatile
в качестве барьера памяти, безусловно, не переносимо.Является ли поведение гарантирует памяти языка C или не из -
volatile
видимому , спорно, хотя лично я считаю , что язык понятен. Сначала у нас есть формальное определение побочных эффектов, C17 5.1.2.3:Стандарт определяет термин «последовательность» как способ определения порядка оценки (исполнения). Определение является формальным и громоздким:
TL; DR вышеупомянутого в основном состоит в том, что в случае, если у нас есть выражение,
A
которое содержит побочные эффекты, оно должно быть выполнено перед другим выражениемB
, в случае еслиB
последовательность упорядочена послеA
.Оптимизация кода на C стала возможной благодаря этой части:
Это означает, что программа может оценивать (выполнять) выражения в порядке, который стандарт предписывает в другом месте (порядок оценки и т. Д.). Но ему не нужно оценивать (выполнять) значение, если оно может сделать вывод, что оно не используется. Например, операция
0 * x
не нуждается в оценкеx
и просто заменяет выражение на0
.Если доступ к переменной не является побочным эффектом. Это означает , что в случае , если
x
естьvolatile
, то необходимо оценить (выполнить) ,0 * x
даже если результат будет всегда 0. Оптимизация не допускается.Кроме того, стандарт говорит о наблюдаемом поведении:
Учитывая все вышеизложенное, соответствующая реализация (компилятор + базовая система) может не выполнять доступ к
volatile
объектам в неупорядоченном порядке, если семантика письменного источника C говорит об обратном.Это означает, что в этом примере
Оба выражения присваивания должны быть оценены и
z = x;
должны быть оценены раньшеz = y;
. Многопроцессорная реализация, которая переносит эти две операции на два разных непоследовательных ядра, не соответствует!Дилемма заключается в том, что компиляторы мало что могут сделать с такими вещами, как кеширование перед извлечением, конвейерная обработка команд и т. Д., Особенно при работе в ОС. И поэтому компиляторы передают эту проблему программистам, говоря им, что барьеры памяти теперь являются обязанностью программиста. В то время как в стандарте C четко указано, что проблема должна решаться компилятором.
Однако компилятору не обязательно решать эту проблему, и поэтому он
volatile
выступает в роли барьера памяти непереносимым. Это стало вопросом качества реализации.источник
z
быть действительно выполнены? (какz = x; z = y;
) Значение будет стерто в следующем утверждении.z
действительно назначается дважды? Откуда вы знаете, что «чтения выполняются»?