![]() | ||||||||||||||||||||||||||||||
![]() |
![]() |
|
|
|||||||||||||||||||||||||||
![]() |
|
Однажды вы читали о ключевом слове volatile…Источник: habrahabr DmitryMe
Сегодня рассмотрим менее экзотический сценарий использования ключевого слова volatile . Стандарт C++ определяет так называемое наблюдаемое поведение как последовательность операций ввода-вывода и чтения-записи данных, объявленных как volatile (1.9/6). В пределах сохранения наблюдаемого поведения компилятору позволено оптимизировать код как угодно. Вот например… Ваш код выделяет память средствами операционной системы, и вы хотите, чтобы операционная система выделила физические страницы памяти под всю запрошенную область. Многие ОС выделяют страницы при первом реальном обращении, а это может приводить к дополнительным задержкам, а вы, например, хотите этих задержек избежать и перенести их на более ранний момент. Вы можете написать такой код:
Этот код проходит по всей области и читает по одному байту из каждой страницы памяти. Одна проблема - компилятор этот код оптимизирует и полностью удалит. Имеет полное право - этот код не влияет на наблюдаемое поведение. Ваши переживания о выделении страниц операционной системой и вызванных этим задержке к наблюдаемому поведению не относятся. Что же делать, что же делать… А, точно! Давайте мы запретим компилятору оптимизировать этот код.
Отлично, в результате… 1. использована #pragma , которая делает код плохо переносимым, плюс… Здесь отлично помогло бы ключевое слово volatile :
И все, достигается ровно нужный эффект - код предписывает компилятору обязательно выполнить чтение с заданным шагом. Оптимизация компилятором не имеет права менять это поведение, потому что теперь последовательность чтений относится к наблюдаемому поведению. Теперь попробуем перезаписать память во имя безопасности и паранойи (это не бред, вот как это бывает в реальной жизни). В том посте упоминается некая волшебная функция SecureZeroMemory() , которая якобы гарантированно перезаписывает нулями указанную область памяти. Если вы используете memset() или эквивалентный ей написанный самостоятельно цикл, например, такой:
для локальной переменной, то есть риск, что компилятор удалит этот цикл, потому что цикл не влияет на наблюдаемое поведение (доводы в том посте к наблюдаемому поведению тоже не относятся). Что же делать, что же делать… А, мы "обманем" компилятор… Вот что можно найти по запросу "prevent memset optimization": 1. замена локальной переменной на переменную в динамической памяти со всеми вытекающими накладными расходами и риском утечки (сообщение в архиве рассылки linux-kernel) У всех этих способов много общих черт - они плохо переносимы и их сложно проверить. Например, вы "обманули" какую-то версию компилятора, а более новая будет иметь более умный анализатор, который догадается, что код не имеет смысла, и удалит его, и сделает так не везде, а только в некоторых местах. Вы можете скомпилировать функцию перезаписи в отдельную единицу трансляции, чтобы компилятор "не увидел", что она делает. После очередной смены компилятора в игру вступит генерация кода линкером (LTCG в Visual C++, LTO в gcc или как это называется в используемом вами компилятором) - и компилятор прозреет и увидит, что перезапись памяти "не имеет смысла", и удалит ее. Не зря появилась поговорка you can"t lie to a compiler . А что если посмотреть на типичную реализацию SecureZeroMemory() ? Она по сути такая:
КРАЙНЕ НЕОЖИДАННО… вопреки всем суевериям зачеркнутое утверждение выше неверно . На самом деле - имеет. Стандарт говорит, что последовательность чтения-записи должна сохраняться только для данных с квалификатором volatile . Вот для таких:
Если сами данные не имеют квалификатора volatile , а квалификатор volatile добавляется указателю на эти данные, чтение-запись этих данных уже не относится к наблюдаемому поведению:
Вся надежда на разработчиков компилятора - в настоящий момент и Visual C++, и gcc не оптимизируют обращения к памяти через указатели с квалификатором volatile - в том числе потому, что это один из важных сценариев использования таких указателей. Не существует гарантированного Стандартом способа перезаписать данные функцией, эквивалентной SecureZeroMemory() , если переменная с этими данными не имеет квалификатора volatile . Точно так же невозможно кодом как в самом начале поста гарантированно прочитать память. Все возможные решения не являются абсолютно переносимыми. Причина этому банальна - это "не нужно". Ситуации, когда переменная с подлежащими записи данными выходит из области видимости, а затем занимаемая ей память переиспользуется под другую переменную и из новой переменной выполняется чтение без предварительной инициализации, относятся к неопределенному поведению. Стандарт ясно говорит, что в таких случаях допустимо любое поведение. Обычно просто читается "мусор", который был записан в эту память раньше. Поэтому с точки зрения Стандарта гарантированная перезапись таких переменных перед выходом из области видимости не имеет смысла. Точно так же не имеет смысла читать память ради чтения памяти. Использование указателей на volatile является, скорее всего, самым эффективным способом решения проблемы. Во-первых, разработчики компиляторов обычно сознательно выключают оптимизацию доступа к памяти. Во-вторых, накладные расходы минимальны. В-третьих, относительно легко проверить, работает этот способ или нет на конкретной реализации, - достаточно посмотреть, какой машинный код будет сгенерирован для тривиальных примеров выше из этого поста. volatile - не только для драйверов и операционных систем. Дмитрий Мещеряков, Ссылки по теме
|
|