Гарантирует ли «volatile» что-либо вообще в переносимом коде C для многоядерных систем?

12

После глядя на кучу из других вопросов и их ответов , я получаю впечатление , что не существует никакого широко распространенного соглашения о том , что «летучий» ключевое слово в C означает точно.

Даже сам стандарт не достаточно ясен для того, чтобы все могли понять, что это значит .

Среди других проблем:

  1. Кажется, он дает разные гарантии в зависимости от вашего оборудования и от вашего компилятора.
  2. Это влияет на оптимизацию компилятора, но не на аппаратную оптимизацию, поэтому на усовершенствованном процессоре, который выполняет свои собственные оптимизации во время выполнения, даже не ясно, может ли компилятор предотвратить любую оптимизацию, которую вы хотите предотвратить. (Некоторые компиляторы генерируют инструкции для предотвращения некоторых аппаратных оптимизаций в некоторых системах, но это никак не стандартизировано.)

Чтобы подвести итог проблемы, кажется (после прочтения много), что «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» вне этих двух очень специфических случаев для многоядерных целей.

Matt
источник
3
Сигналы существуют в переносном С; как насчет глобальной переменной, которая обновляется обработчиком сигнала? Это необходимо для volatileинформирования программы о том, что она может изменяться асинхронно.
Нейт Элдридж
2
@NateEldredge Global, хотя сам по себе волатильный, недостаточно хорош. Это должно быть и атомным.
Евгений Ш.
@EugeneSh .: Да, конечно. Но данный вопрос касается volatileименно того, что, я считаю, необходимо.
Нейт Элдридж
« Хотя координация с кешем L1 ничего не гарантирует в отношении координации с другими потоками » Где недостаточно «координации с кешем L1» для связи с другими потоками?
любопытный парень
1
Возможно, уместно, предложение C ++ об отказе от волатильности , предложение решает многие из проблем, которые вы поднимаете здесь, и, возможно, его результат будет влиять на комитет C
ММ

Ответы:

1

Чтобы подвести итог проблемы, кажется (после прочтения много), что "volatile" гарантирует что-то вроде: Значение будет читаться / записываться не только из / в регистр, но по крайней мере в кэш L1 ядра, в том же порядке, что чтения / записи появляются в коде .

Нет, это абсолютно не так . И это делает volatile практически бесполезным для безопасного кода MT.

Если бы это было так, то volatile было бы неплохо для переменных, совместно используемых несколькими потоками, так как упорядочение событий в кеше L1 - это все, что вам нужно сделать в типичном ЦП (то есть многоядерном или многопроцессорном на материнской плате), способном взаимодействовать. таким образом, что нормальная реализация многопоточности C / C ++ или Java возможна с типичными ожидаемыми затратами (то есть, не большими затратами на большинство атомарных или неконтролируемых операций мьютекса).

Но volatile не обеспечивает какого-либо гарантированного упорядочения (или «видимости памяти») в кэше ни в теории, ни на практике.

(Примечание: следующее основано на правильной интерпретации стандартных документов, намерениях стандарта, исторической практике и глубоком понимании ожиданий авторов компиляторов. Этот подход основан на истории, реальных практиках, а также ожиданиях и понимании реальных людей в реальный мир, который намного сильнее и надежнее, чем синтаксический анализ слов документа, который, как известно, не является звездной спецификацией и который много раз пересматривался.)

На практике volatile гарантирует ptrace-способность, то есть способность использовать отладочную информацию для работающей программы на любом уровне оптимизации , а также тот факт, что отладочная информация имеет смысл для этих энергозависимых объектов:

  • вы можете использовать ptrace(механизм, подобный ptrace) для установки значимых точек останова в точках последовательности после операций, связанных с изменчивыми объектами: вы действительно можете разрывать именно в этих точках (обратите внимание, что это работает, только если вы хотите установить много точек останова, как и любой другой). Оператор C / C ++ может быть скомпилирован во множество различных начальных и конечных точек сборки (как в массово развернутом цикле);
  • в то время как поток выполнения остановлен, вы можете прочитать значение всех изменчивых объектов, поскольку они имеют свое каноническое представление (после ABI для их соответствующего типа); энергонезависимая локальная переменная может иметь нетипичное представление, например, смещенное представление: переменная, используемая для индексации массива, может быть умножена на размер отдельных объектов для упрощения индексации; или его можно заменить указателем на элемент массива (при условии, что все переменные используются аналогичным образом) (подумайте об изменении dx на du в интеграле);
  • Вы также можете изменить эти объекты (если сопоставления памяти позволяют это, поскольку энергозависимый объект со статическим временем жизни, которые имеют постоянные значения, может находиться в диапазоне памяти, отображенном только для чтения).

Volatile гарантирует на практике немного больше, чем строгая интерпретация ptrace: он также гарантирует, что volatile автоматические переменные имеют адрес в стеке, так как они не назначаются регистру, распределение регистров, которое сделало бы манипуляции ptrace более деликатными (компилятор может выводить отладочную информацию, чтобы объяснить, как переменные распределяются по регистрам, но чтение и изменение состояния регистров немного более сложны, чем доступ к адресам памяти).

Обратите внимание, что полная возможность отладки программы, которая учитывает все переменные, изменчивые, по крайней мере, в точках последовательности, обеспечивается режимом «нулевой оптимизации» компилятора, режимом, который все еще выполняет тривиальные оптимизации, такие как арифметические упрощения (обычно нет гарантированного оптимизация во всех режимах). Но volatile сильнее, чем неоптимизация: x-xего можно упростить для целого, xно не для volatile объекта.

Таким образом, volatile означает гарантированную компиляцию как есть , как перевод из исходного кода в двоичный / сборка компилятором системного вызова не является реинтерпретацией, изменением или оптимизацией каким-либо образом компилятором. Обратите внимание, что библиотечные вызовы могут быть или не быть системными вызовами. Многие официальные системные функции на самом деле являются библиотечными функциями, которые предлагают тонкий слой вставки и обычно откладываются до ядра в конце. (В частности getpid, не нужно переходить к ядру, и он вполне может прочитать область памяти, предоставленную ОС, содержащую информацию.)

Изменчивые взаимодействия - это взаимодействия с внешним миром реальной машины , которые должны следовать «абстрактной машине». Они не являются внутренним взаимодействием частей программы с другими частями программы. Компилятор может только рассуждать о том, что он знает, то есть о внутренних частях программы.

Генерация кода для энергозависимого доступа должна следовать за наиболее естественным взаимодействием с этой областью памяти: это не должно вызывать удивления. Это означает, что некоторые энергозависимые обращения должны быть атомарными : если естественный способ чтения или записи представления a longв архитектуре является атомарным, то ожидается, что чтение или запись a volatile longбудет атомарным, поскольку компилятор не должен генерировать глупый неэффективный код для доступа к изменяемым объектам, например, побайтово .

Вы должны быть в состоянии определить это, зная архитектуру. Вам не нужно ничего знать о компиляторе, так как volatile означает, что компилятор должен быть прозрачным .

Но volatile делает не более чем принудительную передачу ожидаемой сборки для наименее оптимизированных для конкретных случаев операций с памятью: volatile семантика означает общую семантику падежа.

Общий случай - то, что делает компилятор, когда у него нет никакой информации о конструкции: f.ex. Вызов виртуальной функции для lvalue через динамическую диспетчеризацию является общим случаем, а прямой вызов переопределителя после определения во время компиляции типа объекта, обозначенного выражением, является частным случаем. У компилятора всегда есть общая обработка всех конструкций, и он следует ABI.

Volatile не делает ничего особенного для синхронизации потоков или обеспечения «видимости памяти»: volatile предоставляет гарантии только на абстрактном уровне, видимом из потока, выполняющего или остановившего поток, то есть внутри ядра ЦП :

  • volatile ничего не говорит о том, какие операции с памятью достигают основной оперативной памяти (вы можете установить конкретные типы кэширования памяти с помощью инструкций по сборке или системных вызовов для получения этих гарантий);
  • volatile не дает никаких гарантий того, когда операции с памятью будут зафиксированы на любом уровне кэша (даже на уровне L1) .

Только второй пункт означает, что volatile бесполезен в большинстве проблем связи между потоками; первый пункт по существу не имеет отношения к любой программной проблеме, которая не включает в себя связь с аппаратными компонентами вне ЦП, но все же на шине памяти.

Свойство volatile, обеспечивающее гарантированное поведение с точки зрения ядра, выполняющего поток, означает, что асинхронные сигналы, доставляемые этому потоку, которые запускаются с точки зрения порядка выполнения этого потока, см. В операциях в порядке исходного кода. ,

Если вы не планируете отправлять сигналы своим потокам (чрезвычайно полезный подход к консолидации информации о текущих запущенных потоках без предварительно согласованной точки остановки), volatile не для вас.

curiousguy
источник
6

Я не эксперт, но у cppreference.com есть то, что мне кажется довольно хорошей информациейvolatile . Вот суть этого:

Каждый доступ (как чтение, так и запись), сделанный через выражение lvalue типа volatile-квалифицированного, считается наблюдаемым побочным эффектом с целью оптимизации и оценивается строго в соответствии с правилами абстрактной машины (то есть все записи завершаются в некоторое время до следующей точки последовательности). Это означает, что в пределах одного потока выполнения энергозависимый доступ не может быть оптимизирован или переупорядочен относительно другого видимого побочного эффекта, который отделен точкой последовательности от энергозависимого доступа.

Это также дает некоторое использование:

Использование летучих

1) статические энергозависимые объекты моделируют порты ввода-вывода с отображением в памяти и статические энергозависимые объекты моделируют входные порты с отображением в памяти, такие как часы реального времени

2) статические энергозависимые объекты типа sig_atomic_t используются для связи с обработчиками сигналов.

3) изменчивые переменные, которые являются локальными для функции, которая содержит вызов макроса setjmp, являются единственными локальными переменными, которые гарантированно сохранят свои значения после возврата longjmp.

4) Кроме того, изменяемые переменные могут использоваться для отключения определенных форм оптимизации, например, для отключения удаления мертвых хранилищ или постоянного сворачивания для микробенчмарков.

И, конечно, упоминается, что volatileэто не полезно для синхронизации потоков:

Обратите внимание, что переменные переменные не подходят для связи между потоками; они не предлагают атомарность, синхронизацию или упорядочение памяти. Чтение из энергозависимой переменной, которая модифицируется другим потоком без синхронизации или одновременного изменения из двух несинхронизированных потоков, является неопределенным поведением из-за гонки данных.

Фред Ларсон
источник
2
В частности, (2) и (3) относятся к переносимому коду.
Нейт Элдридж
2
@TED ​​Несмотря на доменное имя, ссылка на информацию о C, а не C ++
Дэвид Браун
@NateEldredge Вы можете редко использовать longjmpв коде C ++.
любопытный парень
@DavidBrown C и C ++ имеют одинаковое определение наблюдаемого SE и, по сути, одинаковые потоковые примитивы.
любопытный парень
4

Во-первых, исторически существовали различные отклонения в отношении разных толкований значения volatileдоступа и тому подобное. См. Это исследование: Летучие вещества некомпилированы, и что с этим делать .

Помимо различных проблем, упомянутых в этом исследовании, поведение volatileявляется переносимым, за исключением одного из них: когда они действуют как барьеры памяти . Барьер памяти - это некоторый механизм, который предотвращает одновременное неупорядоченное выполнение вашего кода. Использование volatileв качестве барьера памяти, безусловно, не переносимо.

Является ли поведение гарантирует памяти языка C или не из - volatileвидимому , спорно, хотя лично я считаю , что язык понятен. Сначала у нас есть формальное определение побочных эффектов, C17 5.1.2.3:

Доступ к volatileобъекту, изменение объекта, изменение файла или вызов функции, выполняющей любую из этих операций, являются побочными эффектами , которые являются изменениями в состоянии среды выполнения.

Стандарт определяет термин «последовательность» как способ определения порядка оценки (исполнения). Определение является формальным и громоздким:

Последовательность перед - это асимметричное, транзитивное, попарное отношение между оценками, выполняемыми одним потоком, что вызывает частичный порядок среди этих оценок. При любых двух оценках A и B, если A секвенируется перед B, то выполнение A должно предшествовать выполнению B. (И наоборот, если A секвенируется до B, тогда B секвенируется после A.) Если A не секвенируется до или после B, A и B не секвенируются . Оценки A и B являются неопределенно упорядоченными, когда A упорядочены до или после B, но это не определено. 13) Наличие точки последовательности между оценками выражений A и B подразумевается, что каждое вычисление значения и побочный эффект, связанный с A, упорядочивается перед каждым вычислением значения и побочным эффектом, связанным с B. (Краткое изложение точек последовательности приведено в приложении C.)

TL; DR вышеупомянутого в основном состоит в том, что в случае, если у нас есть выражение, Aкоторое содержит побочные эффекты, оно должно быть выполнено перед другим выражением B, в случае если Bпоследовательность упорядочена после A.

Оптимизация кода на C стала возможной благодаря этой части:

В абстрактной машине все выражения оцениваются в соответствии с семантикой. Реальная реализация не должна оценивать часть выражения, если она может сделать вывод, что его значение не используется и что не возникает никаких побочных эффектов (включая любые, вызванные вызовом функции или доступом к энергозависимому объекту).

Это означает, что программа может оценивать (выполнять) выражения в порядке, который стандарт предписывает в другом месте (порядок оценки и т. Д.). Но ему не нужно оценивать (выполнять) значение, если оно может сделать вывод, что оно не используется. Например, операция 0 * xне нуждается в оценке xи просто заменяет выражение на 0.

Если доступ к переменной не является побочным эффектом. Это означает , что в случае , если xесть volatile, то необходимо оценить (выполнить) , 0 * xдаже если результат будет всегда 0. Оптимизация не допускается.

Кроме того, стандарт говорит о наблюдаемом поведении:

Наименьшие требования к соответствующей реализации:

  • Доступ к изменчивым объектам оценивается строго по правилам абстрактной машины.
    / - / Это наблюдаемое поведение программы.

Учитывая все вышеизложенное, соответствующая реализация (компилятор + базовая система) может не выполнять доступ к volatileобъектам в неупорядоченном порядке, если семантика письменного источника C говорит об обратном.

Это означает, что в этом примере

volatile int x;
volatile int y;
z = x;
z = y;

Оба выражения присваивания должны быть оценены и z = x; должны быть оценены раньше z = y;. Многопроцессорная реализация, которая переносит эти две операции на два разных непоследовательных ядра, не соответствует!

Дилемма заключается в том, что компиляторы мало что могут сделать с такими вещами, как кеширование перед извлечением, конвейерная обработка команд и т. Д., Особенно при работе в ОС. И поэтому компиляторы передают эту проблему программистам, говоря им, что барьеры памяти теперь являются обязанностью программиста. В то время как в стандарте C четко указано, что проблема должна решаться компилятором.

Однако компилятору не обязательно решать эту проблему, и поэтому он volatileвыступает в роли барьера памяти непереносимым. Это стало вопросом качества реализации.

Лундин
источник
@curiousguy Не имеет значения.
Лундин
@curiousguy Не имеет значения, если это какой-то целочисленный тип с определителями или без них.
Лундин
Если это простое энергонезависимое целое число, почему избыточные записи должны zбыть действительно выполнены? (как z = x; z = y;) Значение будет стерто в следующем утверждении.
любопытный парень
@curiousguy Потому что чтение изменчивых переменных должно выполняться независимо от указанной последовательности.
Лундин
Тогда zдействительно назначается дважды? Откуда вы знаете, что «чтения выполняются»?
любопытный парень