С. Мейерс - Эффективный и современный C++ (1114942), страница 68
Текст из файла (страница 68)
2 . Эти п равила предписывают, чтобы для объявления типов, не являющихсяссылочными или типами указатепей (что и выпо1111яется о спучае у), квапификаторы c o n s tи vo l a t i l e были опущены. Следовательно, типом у является просто i n t . Это означает, чтоизбыточные чтения и записи у могут быть удалены. В приведенном примере компиляторыдолжны выпо11нять и инициапизацию, и присваивание у, поскольку х объявлена как vol a t i l e ,так что второе чтение х может давать другое значение, отличное о т первого.7.б. Используйте std::atomic дnя параnnеnьности, volatile-дnя особой памяти277Тот факт, что кажущиеся избыточными загрузки и бессмысленные сохранения должны оставаться на месте при работе с особой памятью, объясняет, кстати, почему для такого рода работы не подходят объекты std : : at omic. Компиляторам разрешается устранять такие избыточные операции у std : : atomic.
Код написан не в точности так же, каки для volati le, но если мы на минуту отвлечемся от этого и сосредоточимся на том, чтокомпиляторам разрешается делать, то можно сказать, что концептуально компиляторымогут, получив кодstd: : atomic<int> х ;auto у = х ; 1 1 Концептуально чита етух;11 Концептуально читаетх10;11 Записывает хх = 20;11 Записывает х еще разхх( см . ниже )еще раз ( см . ниже )оптимизировать его доauto у =хх;= 20;/ / Концептуально читает11 Записывает хх( см. ниже )Очевидно, что это неприемлемое поведение при работе с особой памятью.Но если х имеет тип s t d : : atomic, ни одна из этих инструкций компилироваться небудет:auto уу = х;х;// Ошибка 1/ / Ошибка !Дело в том, что копирующие операции в s t d : : atomic удалены (см.
раздел 3.5). И незря. Рассмотрим, что произошло бы, если бы инициализация у значением х компилировалась. Поскольку х имеет тип s t d : : atomic, тип у был бы также выведен как std : : atomic(см. раздел 1 .2). Ранее я отмечал, что одна из лучших возможностей std : : atomic заключается в атомарности всех их операций, но чтобы копирующее конструирование у из хбыло атомарным, компиляторы должны генерировать код для чтения х и записи у какединую атомарную операцию. В общем случае аппаратное обеспечение не в состоянииэто сделать, так что копирующее конструирование типами std : : atomic не поддерживается.
Копирующее присваивание удалено по той же причине, а потому присваивание х переменной у также не компилируется. (Перемещающие операции не объявленыв std : : atomic явно, так что в соответствии с правилами генерации специальных функций, описанных в разделе 3. 1 1 , std : : atomi c не предоставляет ни перемещающего конструирования, ни перемещающего присваивания.)Можно получить значение х в переменную у, но это требует использования функцийчленов load и store типа s t d : : atomic.
Функция-член load атомарно считывает значение std : : atomic, а функция-член store атомарно его записывает. Для инициализации узначением х, после которой выполняется размещение значения х в у, код должен иметьследующий вид:278Глава 7. Параллельные вычисленияstd : : atomic<int> y (x . load ( ) ) ; / / Чтение хy . store (x . load ( ) ) ;/ / Чтение х еще разЭтот код компилируется, но тот факт, что чтение х (с помощью х .
load ( ) ) является отдельным от инициализации или сохранения значения у вызовом функции, делает очевидным то, что нет никаких оснований ожидать, что целая инструкция будет выполняться как единая атомарная операция.Компиляторы могут "оптимизировать" приведенный код, сохраняя значение х в регистре вместо двойного его чтения:rвgistвr=x .
load ( ) ;/ / Чтение хврегистрstd : : atomic<int> y ( rвgistвr) ; 11 Инициализация у1 1 значением регистрау . store ( rвgistвr) ;/ / Сохранение значения11 регистрав уВ результате, как можно видеть, чтение из х выполняется только один раз, и этой разновидности оптимизации следует избегать при работе с особой памятью. (Эта оптимизация не разрешена при работе с переменными vol a t i le.)Таким образом, ситуация должна быть очевидна.•std : : atomic применяется в параллельном программировании, но не для доступак особой памяти.•volat i l e применяется для доступа к особой памяти, но не в параллельном программировании.Поскольку s t d : : atomic и volat i le служат разным целям, они могут использоватьсясовместно:volatile std: : atomic<int> va i ; / / Операции над vai атомарны11 и не могут быть удалены11 при оптимизацииЭтот может оказаться полезным, когда va i соответствует ячейке отображаемого на память ввода-вывода, обращение к которой выполняется несколькими потоками.Последнее примечание: некоторые разработчики предпочитают использовать функции-члены load и store типа std : : atomic даже там, где это не требуется, поскольку эточетко указывает в исходном тексте на то, что данные переменные не являются "обычными': Подчеркивание этого факта не является необоснованным.
Доступ к std: : atomicобычно гораздо медленнее, чем к переменным, не являющимся std : : at omic, и мы ужевидели, что использование s t d : : atomic предотвращает определенное переупорядочениекода, которое иначе было бы разрешено. Вызовы загрузок и сохранений std : : atomic могут тем самым помочь в идентификации потенциальных узких мест масштабируемости.С точки зрения корректности отсутствие вызова store у переменной, предназначеннойдля передачи информации в другие потоки (например, флаг, указывающий доступность7.6.Испопьзуйте std::atomic для параллельности, volatile - для особой памяти279данных), может означать, что эта переменная не объявлена как std : : atomic, хотя должна быть таковой.Однако в большей степени это вопрос стиля, и как таковой он не имеет отношенияк выбору между std : : atomic и volat i le.Сл едует запомнить•std : : at omi c применяется для обращения нескольких потоков к данным без использования мьютексов.
Это инструмент параллельного программирования.•volat i le применяется для памяти, чтения и записи которой не должны удалятьсяпри оптимизации. Это инструмент для работы с особой памятью.280Глава 7. Параллельные вычи слен и яГЛАВА 8Тонко с т иДля каждого общего метода или возможности С++ имеются условия, когда его применение является разумным, и обстоятельства, в которых его не следует применять.Обычно описание того, когда имеет смысл применение некоторого общего метода иливозможности, достаточно простое, но в данной главе описываются два исключения изэтого правила: общий метод (передача по значению) и общая возможность (размещение(emplacement)). Решение об их применении зависит от такого большого количества факторов, что лучший совет, какой я могу дать, сводится к рассмотрению возможности ихприменения.
Тем не менее оба они являются важными составляющими эффективногосовременного программирования на С++, и приведенная в разделах этой главы информация необходима для принятия вами решения об их применении в своих программах.8.1 . Рассмотрите передачу по значениюдnя копируемы х параметров, которые ner кoпереме щаются и всеrда копируютсяНекоторые параметры функций предназначаются для копирования'. Например,функция-член addName может копировать свой параметр в закрытый контейнер. Для эффективности такая функция должна копировать аргументы, являющиеся lvalue, но перемещать аргументы, являющиеся rvalue:class Widget {puЫ ic :void addName ( conвt std : : вtrinq& newName ) / / lvalue ;/ / копируем{ names . push_back ( newName ) ; }// rvalue ;void addName ( s td : : strinq&& newName)/ / перемещаем{ names .
push_back ( std : : move ( newName ) ) ;11 См. применение std : : move в разделе 5 . 31 Вданном разде11е "конирование" параметра в общем с11учае означает его испо11ьзопание как источника дня операций копирования или перемещения. Помните, что в С++ нет терминологического раз11ичин между копированием, вьшонняемым с помощью операции копирования, и копированием с номощью онсрации неремещенин.private :std : : vector<std: : string> name s ;1;Этот способ работает, но требует двух функций, выполняющих, по сути, одни и те жедействия. Это несколько раздражает: надо объявить две функции, реализовать две функции, документировать две функции и наконец поддерживать также две функции! Тьфу.
. .Кроме того, две функции будут и в объектном коде, что при определенных обстоятельствах тоже может напрягать. В данном конкретном случае обе функции, вероятно,будут встраиваемыми, так что этот вопрос не встанет, но если функции не встраиваемые,то в объектном коде будут они обе.Альтернативный подход заключается в том, чтобы сделать addName шаблоном функции, получающей универсальную ссылку (см. раздел 5.2):class Widget {puЫ i c :t:emplate<typename Т>11 Получаем как lvalue,void addName (T&& newName) // так и rvalue ; lvalue11 копируем, rvalue перемещаемnames .