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

После просмотра связки из других вопросов и их ответы , у меня складывается впечатление, что нет широко распространенного соглашения о том, что именно означает ключевое слово "volatile" в Си.

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

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

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

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

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

Кажется, что «use 1» не встречается в переносимом коде, цели которого включают многоядерные системы.

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

Так что " use 2 ", по-видимому, выходит за рамки того, что может обеспечить" volatile ".

USE 3 1063 * единственный бесспорный использование я вижу, чтобы предотвратить неправильное оптимизации доступов с помощью различных переменных, указывающих на ту же область памяти, что компилятор не понимает, это же память. Но это возможно только неоспоримо, потому что люди не говорят об этом - я видел только одно упоминание о нем. И я думал, что стандарт C уже признал, что «разные» указатели (например, разные аргументы на функцию) могут указывать на один и тот же элемент или близлежащие элементы, и уже указывал, что компилятор должен создавать код, который работает даже в таких случаях. Однако я не смог быстро найти эту тему в последнем (500 страниц!) Стандарте.

Так что "использовать 3", возможно, вообще не существует ?

Отсюда мой вопрос:

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


РЕДАКТИРОВАТЬ - обновить

После просмотра последнего стандарта , похоже, что ответ по крайней мере очень ограничено да:
1. В стандарте неоднократно указывается специальная обработка для конкретного типа "volatile sig_atomic_t". Однако в стандарте также говорится, что использование функции сигнала в многопоточной программе приводит к неопределенному поведению. Таким образом, этот вариант использования кажется ограниченным связью между однопоточной программой и ее обработчиком сигналов.
2. Стандарт также устанавливает четкое значение для «volatile» по отношению к setjmp / longjmp. (Пример кода, где это важно, дан в других вопросах и ответах .)

Таким образом, более точный вопрос становится:
Имеет ли "volatile"гарантировать что-либо вообще в переносимом коде C для многоядерных систем, кроме (1) разрешения однопоточной программе получать информацию от своего обработчика сигналов или (2) разрешения коду setjmp видеть переменные, измененные между setjmp и longjmp?

Это все еще вопрос да / нет.

Если "да", было бы здорово, если бы вы могли показать пример ошибки:бесплатный переносимый код, который становится ошибочным, если пропущен «volatile». Если «нет», то я предполагаю, что компилятор может игнорировать «volatile» вне этих двух очень специфических случаев для многоядерных целей.

Ответы [ 3 ]

6 голосов
/ 04 ноября 2019

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

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

Он также дает некоторыеиспользует:

Использование энергозависимых

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

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

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

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

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

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

4 голосов
/ 04 ноября 2019

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

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

1011 * Является ли язык гарантирует поведение памяти 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 ради действия в качестве барьера памятине является переносимымЭто стало вопросом качества реализации.

1 голос
/ 13 ноября 2019

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

...