C ++ 11 представил стандартизированную модель памяти.Что это значит?И как это повлияет на программирование на C ++? - PullRequest
1750 голосов
/ 12 июня 2011

C ++ 11 ввел стандартизированную модель памяти, но что именно это означает?И как это повлияет на программирование на C ++?

Эта статья (автор Гэвин Кларк , который цитирует Херб Саттер ) говорит, что

Модель памяти означает, что код C ++ теперь имеет стандартизированную библиотеку для вызова независимо от того, кто создал компилятор и на какой платформе он работает.Существует стандартный способ управления тем, как разные потоки взаимодействуют с памятью процессора.

"Когда вы говорите о разделении [кода] по различным ядрам, которые есть в стандарте, мы говорим о модели памяти. Мы собираемсячтобы оптимизировать его, не нарушая следующие предположения, которые люди собираются сделать в коде: " Саттер сказал.

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

Программисты C ++ раньше разрабатывали многопоточные приложения, поэтому какое это имеет значение, если это потоки POSIX, или потоки Windows, или потоки C ++ 11?Каковы преимущества?Я хочу понять детали низкого уровня.

У меня также возникает ощущение, что модель памяти C ++ 11 так или иначе связана с поддержкой многопоточности C ++ 11, поскольку я часто вижу эти два вместе.Если это так, как именно?Почему они должны быть связаны?

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

Ответы [ 6 ]

2028 голосов
/ 12 июня 2011

Во-первых, вы должны научиться мыслить как юрист по языку.

Спецификация C ++ не содержит ссылок на какой-либо конкретный компилятор, операционную систему или процессор.Он ссылается на абстрактную машину , которая является обобщением реальных систем.В мире Language Lawyer работа программиста заключается в написании кода для абстрактной машины;работа компилятора состоит в том, чтобы актуализировать этот код на конкретной машине.Жестко кодируя спецификацию, вы можете быть уверены, что ваш код будет компилироваться и выполняться без изменений в любой системе с совместимым компилятором C ++, будь то сегодня или через 50 лет.

Абстрактная машина в C +Спецификация + 98 / C ++ 03 принципиально однопоточная.Таким образом, невозможно написать многопоточный код C ++, который является «полностью переносимым» по отношению к спецификации.В спецификации даже ничего не говорится о атомарности загрузок и сохранений памяти или порядке , в котором могут происходить загрузки и сохранения, не говоря уже о таких вещах, как мьютексы.

Конечно, вы можете написать многопоточный код на практике для конкретных конкретных систем, таких как pthreads или Windows.Но не существует стандартного способа написания многопоточного кода для C ++ 98 / C ++ 03.

Абстрактная машина в C ++ 11 имеет многопоточный дизайн.Он также имеет четко определенную модель памяти ;то есть, он говорит, что компилятор может и не может делать, когда речь заходит о доступе к памяти.

Рассмотрим следующий пример, где к паре глобальных переменных одновременно обращаются два потока:

           Global
           int x, y;

Thread 1            Thread 2
x = 17;             cout << y << " ";
y = 37;             cout << x << endl;

Что может выводить поток 2?

В C ++ 98 / C ++ 03 это даже не неопределенное поведение;сам вопрос: бессмысленно , потому что стандарт не предусматривает ничего, называемого "потоком".

В C ++ 11 результатом является неопределенное поведение, поскольку загрузка и хранение не должны быть атомарнымив общем.Что может показаться не большим улучшением ... И само по себе это не так.

Но с C ++ 11 вы можете написать это:

           Global
           atomic<int> x, y;

Thread 1                 Thread 2
x.store(17);             cout << y.load() << " ";
y.store(37);             cout << x.load() << endl;

Теперь все становится намного лучшеинтереснее.Прежде всего, поведение здесь определено .Поток 2 теперь может печатать 0 0 (если он выполняется до потока 1), 37 17 (если он выполняется после потока 1) или 0 17 (если он выполняется после того, как поток 1 назначает x, но до того, как он назначает y).

То, что он не может распечатать, это 37 0, потому что режим по умолчанию для атомарных загрузок / хранилищ в C ++ 11 - обеспечить последовательную согласованность .Это просто означает, что все загрузки и хранилища должны быть «такими, как если бы» происходили в том порядке, в котором вы их записали в каждом потоке, а операции между потоками могут чередоваться, как нравится системе.Таким образом, стандартное поведение атома обеспечивает как атомарность , так и упорядочение для нагрузок и хранилищ.

Теперь на современном CPU обеспечение последовательной согласованности может быть дорогостоящим.В частности, компилятор, вероятно, будет создавать полноценные барьеры памяти между каждым доступом здесь.Но если ваш алгоритм может терпеть неупорядоченные загрузки и хранения;то есть, если это требует атомарности, но не упорядоченности;то есть, если он может допустить 37 0 как вывод этой программы, вы можете написать это:

           Global
           atomic<int> x, y;

Thread 1                            Thread 2
x.store(17,memory_order_relaxed);   cout << y.load(memory_order_relaxed) << " ";
y.store(37,memory_order_relaxed);   cout << x.load(memory_order_relaxed) << endl;

Чем современнее ЦП, тем выше вероятность, что он будет быстрее, чем в предыдущем примере.

Наконец, если вам просто нужно сохранить порядок определенных загрузок и хранилищ, вы можете написать:

           Global
           atomic<int> x, y;

Thread 1                            Thread 2
x.store(17,memory_order_release);   cout << y.load(memory_order_acquire) << " ";
y.store(37,memory_order_release);   cout << x.load(memory_order_acquire) << endl;

Это возвращает нас к заказанным загрузкам и хранилищам - поэтому 37 0 нетдольше возможный вывод - но это происходит с минимальными накладными расходами.(В этом тривиальном примере результат такой же, как в полномасштабной последовательной согласованности; в более крупной программе этого не произойдет.)

Конечно, если вы хотите видеть только выходные данные 0 0или 37 17, вы можете просто обернуть мьютекс вокруг исходного кода.Но если вы читали это далеко, я уверен, что вы уже знаете, как это работает, и этот ответ уже длиннее, чем я предполагал: -).

Итак, суть. Мьютексы великолепны, и C ++ 11 их стандартизирует. Но иногда по соображениям производительности вам нужны низкоуровневые примитивы (например, классический дважды проверенный шаблон блокировки ). Новый стандарт предоставляет высокоуровневые гаджеты, такие как мьютексы и условные переменные, а также низкоуровневые гаджеты, такие как атомарные типы и различные варианты барьера памяти. Так что теперь вы можете писать сложные высокопроизводительные параллельные процедуры полностью на языке, указанном в стандарте, и вы можете быть уверены, что ваш код будет компилироваться и выполняться без изменений как в современных системах, так и в завтрашних.

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

Подробнее об этом см. в этом блоге .

317 голосов
/ 30 августа 2013

Я просто приведу аналогию, с которой я понимаю модели согласованности памяти (или модели памяти, для краткости).Он вдохновлен оригинальной работой Лесли Лампорта «Время, часы и порядок событий в распределенной системе» .Аналогия уместна и имеет фундаментальное значение, но может быть излишней для многих людей.Тем не менее, я надеюсь, что он предоставляет мысленный образ (графическое представление), который облегчает рассуждения о моделях согласованности памяти.

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

Цитата из «Учебник по согласованности памяти и согласованности кэша»

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

Этот глобальный порядок памяти может варьироваться от одного запуска программы к другому и может быть неизвестен заранее.Характерной особенностью SC является набор горизонтальных срезов на диаграмме адрес-пространство-время, представляющих плоскостей одновременности (т.е. образы памяти).На данной плоскости все его события (или значения памяти) являются одновременными.Существует понятие Абсолютное время , в котором все потоки согласовывают, какие значения памяти являются одновременными.В SC в каждый момент времени существует только один образ памяти, общий для всех потоков.То есть в каждый момент времени все процессоры согласовывают образ памяти (т. Е. Совокупное содержимое памяти).Это означает не только то, что все потоки просматривают одинаковую последовательность значений для всех областей памяти, но также и то, что все процессоры наблюдают одинаковые комбинации значений всех переменных.Это то же самое, что сказать, что все операции с памятью (во всех ячейках памяти) наблюдаются в одном и том же общем порядке всеми потоками.

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

[Изображение из Википедии] Picture from Wikipedia

Читатели, знакомые с Специальной теорией относительности Эйнштейна , заметят то, на что я намекаю. Перевод слов Минковского в область моделей памяти: адресное пространство и время являются тенями адресного пространства-времени. В этом случае каждый наблюдатель (т. Е. Поток) будет проецировать тени событий (т. Е. Память хранит / загружает) на свою собственную мировую линию (т. Е. На свою временную ось) и свою собственную плоскость одновременности (свою ось адресного пространства). , Потоки в модели памяти C ++ 11 соответствуют наблюдателям , которые движутся относительно друг друга в специальной теории относительности. Последовательная согласованность соответствует галилеевскому пространству-времени (т. Е. Все наблюдатели соглашаются в одном абсолютном порядке событий и общем ощущении одновременности).

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

В относительности некоторый порядок восстанавливается в, казалось бы, хаотической картине частично упорядоченных событий, поскольку единственное временное упорядочение, с которым согласны все наблюдатели, - это упорядочение среди «подобных времени» событий (т. Е. Тех событий, которые в принципе связаны с любым частица движется медленнее, чем скорость света в вакууме). Только связанные с временем события инвариантно упорядочены. Время в физике, Крейг Каллендер .

В модели памяти C ++ 11 аналогичный механизм (модель согласованности получения-выпуска) используется для установления этих локальных причинно-следственных связей .

Чтобы дать определение согласованности памяти и мотивации отказа от SC, я процитирую из «Учебник по согласованности памяти и согласованности кэша»

Для машины с общей памятью модель согласованности памяти определяет архитектурно видимое поведение ее системы памяти. Критерий корректности поведения разделов ядра одного процессора между « один правильный результат » и « много неправильных альтернатив ». Это связано с тем, что архитектура процессора требует, чтобы выполнение потока преобразовывало заданное входное состояние в одно четко определенное выходное состояние, даже на ядре не в порядке. Однако модели согласованности совместно используемой памяти касаются загрузки и хранения нескольких потоков и обычно допускают много правильных выполнений , в то же время запрещая многие (более) неправильные. Возможность множественного правильного выполнения обусловлена ​​тем, что ISA позволяет одновременно выполнять несколько потоков, часто с множеством возможных законных чередований инструкций из разных потоков.

Расслабленный или слабый Модели согласованности памяти мотивированы тем, что большинство упорядочений памяти в сильных моделях не требуется. Если поток обновляет десять элементов данных, а затем флаг синхронизации, программисты обычно не заботятся о том, обновляются ли элементы данных по порядку относительно друг друга, а только о том, что все элементы данных обновляются до обновления флага (обычно реализуются с использованием инструкций FENCE). ). Расслабленные модели стремятся использовать эту повышенную гибкость упорядочения и сохраняют только те заказы, которые программисты « требуют », чтобы получить как более высокую производительность, так и правильность SC. Например, в определенных архитектурах буферы записи FIFO используются каждым ядром для хранения результатов подтвержденных (удаленных) хранилищ перед записью результатов в кэши. Эта оптимизация повышает производительность, но нарушает СЦ. Буфер записи скрывает задержку обслуживания пропуска магазина. Поскольку магазины являются обычным делом, возможность избежать остановки на большинстве из них является важным преимуществом. Для одноядерного процессора буфер записи может быть сделан архитектурно невидимым, гарантируя, что загрузка по адресу A возвращает значение самого последнего хранилища в A, даже если одно или несколько хранилищ в A находятся в буфере записи. Обычно это делается либо путем обхода значения самого последнего хранилища в A для загрузки из A, где «самое последнее» определяется порядком программы, либо путем остановки загрузки A, если хранилище для A находится в буфере записи , Когда используется несколько ядер, у каждого будет свой обходной буфер записи. Без буферов записи аппаратное обеспечение - это SC, но с буферами записи - нет, что делает архитектурно видимые буферы записи в многоядерном процессоре.

Переупорядочение в хранилище может произойти, если ядро ​​имеет буфер записи без FIFO, который позволяет магазинам отправляться в другом порядке, нежели в порядке, в котором они были введены. Это может произойти, если первое хранилище пропадает в кеше, когда второе попадает, или если второе хранилище может объединиться с более ранним хранилищем (то есть до первого хранилища). Изменение порядка загрузки и загрузки также может происходить на ядрах с динамическим планированием, которые выполняют инструкции вне программного порядка. Это может вести себя так же, как и переупорядочение хранилищ на другом ядре (можете ли вы привести пример чередования между двумя потоками?). Переупорядочение более ранней загрузки с более поздним хранилищем (переупорядочение хранилища загрузок) может вызвать много неправильных действий, таких как загрузка значения после снятия блокировки, которая защищает его (если хранилище является операцией разблокировки). Обратите внимание, что переупорядочения при загрузке могут также возникать из-за локального обхода в обычно реализуемом буфере записи FIFO, даже с ядром, которое выполняет все инструкции в программном порядке.

Поскольку согласованность кэша и согласованность памяти иногда путаются, полезно также иметь такую ​​цитату:

В отличие от мошенникаsistency, когерентность кэша не видна программному обеспечению и не требуется.Согласованность стремится сделать кэши системы с общей памятью такими же функционально невидимыми, как кэши в одноядерной системе.Правильная согласованность гарантирует, что программист не сможет определить, есть ли у системы кеши и где она анализирует результаты загрузки и сохранения.Это связано с тем, что правильная согласованность гарантирует, что кеширование никогда не разрешит новое или отличное функциональное поведение (программисты все еще могут вывести вероятную структуру кэша, используя хронирование информация).Основное назначение протоколов когерентности кэша заключается в поддержании инварианта «единица записи-несколько читателей» (SWMR) для каждой области памяти.Важное различие между согласованностью и согласованностью заключается в том, что согласованность указывается на для каждой ячейки памяти , тогда как согласованность указывается в отношении все областей памяти.

Продолжая нашу ментальную картину, инвариант SWMR соответствует физическому требованию, что в любой одной точке может находиться не более одной частицы, но может быть неограниченное количество наблюдателейв любом месте.

100 голосов
/ 20 декабря 2013

Теперь это вопрос нескольких лет, но, будучи очень популярным, стоит упомянуть фантастический ресурс для изучения модели памяти C ++ 11. Я не вижу смысла подводить итоги его выступления, чтобы дать ему еще один полный ответ, но, учитывая, что это парень, который на самом деле написал стандарт, я думаю, что стоит посмотреть доклад.

Херб Саттер в течение трех часов рассказывает о модели памяти C ++ 11 под названием «Атомное <> оружие», доступной на сайте Channel9 - часть 1 и часть 2, Доклад довольно технический и охватывает следующие темы:

  1. Оптимизации, расы и модель памяти
  2. Заказ - Что: Приобретение и Выпуск
  3. Порядок - Как: Мьютексы, Атомика и / или Заборы
  4. Другие ограничения на компиляторы и оборудование
  5. Код Gen & Performance: x86 / x64, IA64, POWER, ARM
  6. Расслабленная атомика

Речь идет не об API, а о рассуждениях, предыстории, скрытых и скрытых факторах (знаете ли вы, что к стандарту добавлена ​​расслабленная семантика только потому, что POWER и ARM не поддерживают синхронизированную нагрузку эффективно ?).

73 голосов
/ 12 июня 2011

Это означает, что стандарт теперь определяет многопоточность и определяет, что происходит в контексте нескольких потоков. Конечно, люди использовали разные реализации, но это все равно, что спрашивать, почему у нас должен быть std::string, когда мы все могли бы использовать класс string домашнего проката.

Когда вы говорите о потоках POSIX или Windows, это немного иллюзия, поскольку на самом деле вы говорите о потоках x86, поскольку это аппаратная функция для одновременного запуска. Модель памяти C ++ 0x дает гарантии, будь то x86, ARM, MIPS или что-нибудь еще, что вы можете придумать.

54 голосов
/ 26 июля 2011

Для языков, не указывающих модель памяти, вы пишете код для языка и модели памяти, определенной архитектурой процессора. Процессор может выбрать изменение порядка доступа к памяти для повышения производительности. Таким образом, , если ваша программа имеет гонки данных (гонка данных - это когда несколько ядер / гиперпотоков могут одновременно обращаться к одной и той же памяти), тогда ваша программа не является кроссплатформенной из-за своей зависимости от процессора модель памяти. Вы можете обратиться к руководствам по программному обеспечению Intel или AMD, чтобы узнать, как процессоры могут переупорядочивать обращения к памяти.

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

Интересно, что компиляторы Microsoft для C ++ имеют семантику приобретения / выпуска для volatile, которое является расширением C ++ для решения проблемы отсутствия модели памяти в C ++ http://msdn.microsoft.com/en-us/library/12a04hfd(v=vs.80).aspx. Однако, учитывая, что Windows работает только на x86 / x64, это мало что говорит (модели памяти Intel и AMD позволяют легко и эффективно реализовать семантику получения / выпуска на языке).

25 голосов
/ 12 июня 2011

Если вы используете мьютексы для защиты всех ваших данных, вам не нужно беспокоиться.Мьютексы всегда обеспечивали достаточные гарантии упорядочения и видимости.

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

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

...