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


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

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

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

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

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

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

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

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

6 1585

6 ответов:

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

спецификация 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;

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

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

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

для получения дополнительной информации об этом материале см. этот блог.

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

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

цитирую "праймер по согласованности памяти и когерентности кэша"

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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