Может ли компилятор иногда кешировать переменную, объявленную как volatile

Из того, что я знаю, компилятор никогда не оптимизирует переменную, которая объявлена ​​как volatile, Тем не менее, у меня есть массив объявлен как это.

volatile long array[8];

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

НОТА: Я не пользуюсь volatile для синхронизации потоков, поэтому, пожалуйста, перестаньте давать мне ответы, такие как использование блокировки или атомарной переменной. Я знаю разницу между изменчивыми, атомарными переменными и мьютексами. Также обратите внимание, что архитектура x86 имеет упреждающую согласованность кэша. Также я читаю переменную достаточно долго после того, как она предположительно изменена другим потоком. Даже после долгого времени поток чтения не может видеть измененное значение.

12

Решение

Но компилятор в принципе не должен кэшировать переменную, не так ли?

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

[Редактировать: По крайней мере, он должен делать это до того момента, когда реализация считает, что значение по этому адресу является «наблюдаемым». Как указывает Дитмар в своем ответе, реализация может заявить, что нормальная память «не может наблюдаться». Это стало бы неожиданностью для людей, использующих отладчики, mprotectили другой материал, выходящий за рамки стандарта, но он может соответствовать в принципе.]

В C ++ 03, который вообще не рассматривает потоки, это зависит от реализации, чтобы определить, что означает «доступ к адресу» при работе в потоке. Такие детали называются «модель памяти». Pthreads, например, позволяет кешировать все потоки памяти для каждого потока, включая переменные. IIRC, MSVC обеспечивает гарантию того, что переменные переменного размера подходящего размера являются атомарными, и это позволит избежать кеширования (скорее всего, оно сбрасывается до единого связного кэша для всех ядер). Причина, по которой он предоставляет такую ​​гарантию, заключается в том, что разумно Дешево сделать это на Intel — Windows действительно заботится только об архитектурах на базе Intel, тогда как Posix занимается более экзотическими вещами.

C ++ 11 определяет модель памяти для многопоточности и говорит, что это гонка данных (т.е. volatile не убедитесь, что чтение в одном потоке упорядочено относительно записи в другом потоке). Два доступа могут быть упорядочены в определенном порядке, упорядочены в неопределенном порядке (стандарт может сказать «неопределенный порядок», я не помню) или не упорядочены вообще. Совсем нет последовательности — это плохо — если любой из двух неупорядоченных обращений является записью, то поведение не определено.

Ключевым моментом здесь является подразумеваемое «и затем» в «Я изменяю элемент из потока И ТОГДА поток, читающий его, не замечает изменения». Вы предполагаете, что операции являются последовательными, но это не так. Что касается потока чтения, то если вы не используете какую-либо синхронизацию, запись в другой поток еще не обязательно произошла. И на самом деле это еще хуже — вы можете подумать из того, что я только что написал, что не указан только порядок операций, но на самом деле поведение программы с гонкой данных не определено.

7

Другие решения

С

Что изменчиво делает:

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

Что не изменчиво:

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

Что изменчиво может или не может сделать:

  • Он может или не может быть реализован компилятором для обеспечения барьера памяти, для защиты от проблем кэша команд / канала команд / переупорядочения команд в многоядерной среде. Вы никогда не должны предполагать, что volatile делает это за вас, если в документации компилятора не указано иное.
3

С volatile Вы можете только навязать, что переменная перечитывается всякий раз, когда вы используете ее значение. Это не гарантирует, что различные значения / представления, которые присутствуют на разных уровнях вашей архитектуры, являются согласованными.

Чтобы получить такие гарантии, вам понадобятся новые утилиты C11 и C ++ 1, касающиеся атомарного доступа и барьеров памяти. Многие компиляторы реализуют их уже с точки зрения расширения. Например, в семействе gcc (clang, icc и т. Д.) Встроенные функции начинаются с префикса __sync реализовать это.

2

Volatile Ключевое слово только гарантирует, что компилятор не будет использовать регистр для этой переменной. Таким образом каждый доступ к этой переменной будет идти и читать ячейку памяти. Теперь я предполагаю, что у вас есть согласованность кэша между несколькими процессорами в вашей архитектуре. Так что, если один процессор пишет, а другой читает, то он должен быть виден в нормальных условиях. Тем не менее, вы должны рассмотреть угловые случаи. Предположим, что переменная находится в конвейере одного ядра процессора, а другой процессор пытается прочитать ее, предполагая, что она была записана, и возникает проблема. Поэтому, по сути, общие переменные должны быть либо защищены блокировками, либо должны быть защищены с помощью правильного использования барьерного механизма.

2

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

Обратите внимание, что те аспекты поведения, которые считаются «наблюдаемыми», могут быть в определенной степени определены реализацией. Если реализация документирует, что она не предназначена для использования на оборудовании, которое использует доступ к основной ОЗУ для запуска необходимых видимых извне действий, тогда доступ к основной ОЗУ не будет «наблюдаемым» в этой реализации. Реализация будет совместима с аппаратным обеспечением, которое способно физически наблюдать такие обращения, если ничто не заботит, действительно ли такие обращения были замечены. Однако, если такие доступы были необходимы, как это было бы, если бы доступ рассматривался как «наблюдаемый», компилятор не требовал бы совместимости и, следовательно, не давал никаких обещаний.

2

Для C ++:

Из того, что я знаю, компилятор никогда не оптимизирует переменную, объявленную как volatile.

Ваша предпосылка неверна. volatile является подсказкой для компилятора и фактически ничего не гарантирует. Компиляторы могут предотвратить некоторые оптимизации volatile переменные, но это все.

volatile это не замок, не пытайтесь использовать его как таковой.

7.1.5.1

7) [Примечание: volatile является подсказкой для реализации, чтобы избежать
агрессивная оптимизация с участием объекта, потому что значение
объект может быть изменен средствами, не обнаруживаемыми реализацией.
См. 1.9 для подробной семантики. Вообще семантика изменчива
должны быть такими же в C ++, как и в C.

1

Volatile влияет только на переменную, перед которой он находится. Вот в вашем примере указатель. Ваш код: volatile long array [8], указатель на первый элемент массива volatile, а не его содержимое. (то же самое для объектов любого вида)

вы можете адаптировать его как в
Как мне объявить массив, созданный с помощью malloc, как изменчивый в c ++

1

volatile ключевое слово имеет ничего такого делать с параллелизмом в C ++ совсем! Он используется для предотвращения использования компилятором предыдущего значения, то есть компилятор будет генерировать код, обращающийся к volatile значение каждый раз, когда осуществляется доступ в коде. Основным назначением являются такие вещи, как ввод-вывод с отображением в память. Тем не менее, использование volatile имеет нет влияет на то, что процессор делает при чтении обычной памяти: если у процессора нет оснований полагать, что значение изменилось в памяти, например, из-за отсутствия директивы синхронизации, он может просто использовать значение из своего кэша. Для связи между потоками необходима некоторая синхронизация, например, std::atomic<T>заблокировать std::mutex, так далее.

1
По вопросам рекламы [email protected]