(495) 925-0049, ITShop интернет-магазин 229-0436, Учебный Центр 925-0049
  Главная страница Карта сайта Контакты
Поиск
Вход
Регистрация
Рассылки сайта
 
 
 
 
 

Однажды вы читали о ключевом слове volatile…

Источник: habrahabr
DmitryMe

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

Сегодня рассмотрим менее экзотический сценарий использования ключевого слова  volatile .

Стандарт C++ определяет так называемое наблюдаемое поведение как последовательность операций ввода-вывода и чтения-записи данных, объявленных как  volatile  (1.9/6). В пределах сохранения наблюдаемого поведения компилятору позволено оптимизировать код как угодно.

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

for( char* ptr = start; ptr < start + size; ptr += MemoryPageSize ) {
     *ptr;
}

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

Что же делать, что же делать… А, точно! Давайте мы запретим компилятору оптимизировать этот код.

#pragma optimize( "", off )
for( char* ptr = start; ptr < start + size; ptr += MemoryPageSize ) {
     *ptr;
}
#pragma optimize( "", on )

Отлично, в результате…

1. использована  #pragma , которая делает код плохо переносимым, плюс…
2. оптимизация выключается полностью, а это увеличивает объем машинного кода в три раза, плюс в Visual C++, например, эта  #pragma  может быть использована только снаружи функции, соответственно, рассчитывать на встраивание этого кода в вызывающий код и дальнейшую оптимизацию тоже не приходится.

Здесь отлично помогло бы ключевое слово  volatile :

for( volatile char* ptr = start; ptr < start + size; ptr += MemoryPageSize ) {
     *ptr;
}

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

Теперь попробуем перезаписать память во имя безопасности и паранойи (это не бред, вот как это бывает в реальной жизни). В том посте упоминается некая волшебная функция  SecureZeroMemory() , которая якобы гарантированно перезаписывает нулями указанную область памяти. Если вы используете memset()  или эквивалентный ей написанный самостоятельно цикл, например, такой:

for( size_t index = 0; index < size; index++ )
     ptr[index] = 0;

для локальной переменной, то есть риск, что компилятор удалит этот цикл, потому что цикл не влияет на наблюдаемое поведение (доводы в том посте к наблюдаемому поведению тоже не относятся).

Что же делать, что же делать… А, мы "обманем" компилятор… Вот что можно найти по запросу "prevent memset optimization":

1. замена локальной переменной на переменную в динамической памяти со всеми вытекающими накладными расходами и риском утечки (сообщение в архиве рассылки linux-kernel)
2. макрос с ассемблерной магией (сообщение в архиве рассылки linux-kernel
3. предложение использовать специальный символ препроцессора, который запрещает встраивание  memset()  по месту и затрудняет компилятору оптимизацию (естественно, такая возможность должна быть поддержана в используемой версии библиотеки, плюс Visual C++ 10 умеет оптимизировать даже код функций, помеченных как не подлежащие встраиванию)
4. всевозможные последовательности чтения-записи с использованием глобальных переменных (кода становится заметно больше и такой код не потокобезопасен)
5. последующее чтение с сообщением об ошибке в случае, если считаны не те данные, что были записаны (компилятор имеет право заметить, что "не тех" данных оказаться не может, и удалить этот код)

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

Вы можете скомпилировать функцию перезаписи в отдельную единицу трансляции, чтобы компилятор "не увидел", что она делает. После очередной смены компилятора в игру вступит генерация кода линкером (LTCG в Visual C++, LTO в gcc или как это называется в используемом вами компилятором) - и компилятор прозреет и увидит, что перезапись памяти "не имеет смысла", и удалит ее.

Не зря появилась поговорка  you can"t lie to a compiler .

А что если посмотреть на типичную реализацию  SecureZeroMemory() ? Она по сути такая:

volatile char *volatilePtr = static_cast<volatile char*>(ptr);
for( size_t index; index < size; index++ )
        * volatilePtr = 0;
}

И все - компилятор более не имеет права удалять запись…

КРАЙНЕ НЕОЖИДАННО… вопреки всем суевериям  зачеркнутое утверждение выше неверно .

На самом деле - имеет. Стандарт говорит, что последовательность чтения-записи должна сохраняться только для данных с квалификатором  volatile . Вот для таких:

volatile buffer[size];

Если сами данные не имеют квалификатора  volatile , а квалификатор  volatile добавляется указателю на эти данные, чтение-запись этих данных уже не относится к наблюдаемому поведению:

buffer[size];
SecureZeroMemory(buffer, sizeof(buffer));

Вся надежда на разработчиков компилятора - в настоящий момент и Visual C++, и gcc не оптимизируют обращения к памяти через указатели с квалификатором volatile  - в том числе потому, что это один из важных сценариев использования таких указателей.

Не существует гарантированного Стандартом способа перезаписать данные функцией, эквивалентной  SecureZeroMemory() , если переменная с этими данными не имеет квалификатора  volatile . Точно так же невозможно кодом как в самом начале поста гарантированно прочитать память. Все возможные решения не являются абсолютно переносимыми.

Причина этому банальна - это "не нужно".

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

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

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

volatile  - не только для драйверов и операционных систем.

Дмитрий Мещеряков,
департамент продуктов для разработчиков

Ссылки по теме


 Распечатать »
 Правила публикации »
  Написать редактору 
 Рекомендовать » Дата публикации: 06.12.2012 
 

Магазин программного обеспечения   WWW.ITSHOP.RU
Quest Software. TOAD for SQL Server Xpert Edition
СУБД Линтер Бастион. Серверная лицензия. 5 клиентских подключений
erwin Data Modeler Standard Edition r9.7 - Product plus 1 Year Enterprise Maintenance Commercial
SmartBear AQtime Pro - Node-Locked License (Includes 1 Year Maintenance)
JIRA Software Commercial (Cloud) Standard 10 Users
 
Другие предложения...
 
Курсы обучения   WWW.ITSHOP.RU
 
Другие предложения...
 
Магазин сертификационных экзаменов   WWW.ITSHOP.RU
 
Другие предложения...
 
3D Принтеры | 3D Печать   WWW.ITSHOP.RU
 
Другие предложения...
 
Новости по теме
 
Рассылки Subscribe.ru
Информационные технологии: CASE, RAD, ERP, OLAP
Новости ITShop.ru - ПО, книги, документация, курсы обучения
СУБД Oracle "с нуля"
OS Linux для начинающих. Новости + статьи + обзоры + ссылки
Новые материалы
Один день системного администратора
Adobe Photoshop: алхимия дизайна
 
Статьи по теме
 
Новинки каталога Download
 
Исходники
 
Документация
 
 



    
rambler's top100 Rambler's Top100