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


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

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

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

«Когда вы говорите о разделении   [код] в разных ядрах,   в стандарте мы говорим о   модель памяти. Мы собираемся   оптимизировать его, не нарушая   после предположений люди идут   сделать в коде, " Sutter сказал.

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

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

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

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


1526


источник


Ответы:


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

Спецификация C ++ не ссылается ни на какой конкретный компилятор, операционную систему или процессор. Он ссылается на абстрактная машина это обобщение реальных систем. В мире юристов, работа программиста заключается в написании кода для абстрактной машины; работа компилятора заключается в том, чтобы реализовать этот код на конкретной машине. Если вы строго кодируете спецификацию, вы можете быть уверены, что ваш код будет компилироваться и запускаться без изменений в любой системе с совместимым компилятором 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;

Что могло бы вывести Thread 2?

В C ++ 98 / C ++ 03 это даже не неопределенное поведение; сам вопрос бессмысленный потому что стандарт не рассматривает ничего, называемое «нитью».

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

Но с 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(если он работает до Thread 1), 37 17(если он работает после Thread 1) или 0 17(если он запускается после того, как Thread 1 назначает x, но до того, как он назначит y).

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

Теперь, на современном процессоре, обеспечение последовательной согласованности может быть дорогостоящим. В частности, компилятор, вероятно, испускает полномасштабные барьеры памяти между каждым доступом здесь. Но если ваш алгоритм может терпеть неуправляемые нагрузки и магазины; т.е. если он требует атомарности, но не упорядочивает; то есть, если он может терпеть 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 их стандартизирует. Но иногда по соображениям производительности вам нужны примитивы нижнего уровня (например, классические двойной контроль блокировки ). Новый стандарт обеспечивает высокоуровневые гаджеты, такие как мьютексы и переменные состояния, а также предоставляет низкоуровневые гаджеты, такие как атомные типы и различные варианты защиты памяти. Итак, теперь вы можете писать сложные, высокопроизводительные параллельные подпрограммы полностью на языке, указанном стандартом, и вы можете быть уверены, что ваш код будет компилироваться и работать без изменений как на сегодняшних системах, так и на завтрашнем.

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

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


1759



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

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

Цитата из «Праймер по согласованности памяти и согласованности кеша»

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

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

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

[Иллюстрация из Википедии] Picture from Wikipedia

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

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

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

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

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

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

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

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

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

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

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


271



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

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

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

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


76



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

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


66



For languages not specifying a memory model, you are writing code for the language and the memory model specified by the processor architecture. The processor may choose to re-order memory accesses for performance. So, if your program has data races (a data race is when it's possible for multiple cores / hyper-threads to access the same memory concurrently) then your program is not cross platform because of its dependence on the processor memory model. You may refer to the Intel or AMD software manuals to find out how the processors may re-order memory accesses.

Very importantly, locks (and concurrency semantics with locking) are typically implemented in a cross platform way... So if you are using standard locks in a multithreaded program with no data races then you don't have to worry about cross platform memory models.

Interestingly, Microsoft compilers for C++ have acquire / release semantics for volatile which is a C++ extension to deal with the lack of a memory model in C++ http://msdn.microsoft.com/en-us/library/12a04hfd(v=vs.80).aspx. However, given that Windows runs on x86 / x64 only, that's not saying much (Intel and AMD memory models make it easy and efficient to implement acquire / release semantics in a language).


49



If you use mutexes to protect all your data, you really shouldn't need to worry. Mutexes have always provided sufficient ordering and visibility guarantees.

Now, if you used atomics, or lock-free algorithms, you need to think about the memory model. The memory model describes precisely when atomics provide ordering and visibility guarantees, and provides portable fences for hand-coded guarantees.

Previously, atomics would be done using compiler intrinsics, or some higher level library. Fences would have been done using CPU-specific instructions (memory barriers).


22