Что на самом деле делает memory_order_consume?

Из ссылки: В чем разница между загрузкой/сохранением расслабленной атомарной и обычной переменной?

Меня очень впечатлил этот ответ:

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

Сегодня я прочитал ссылку ниже: https://preshing.com/20140709/цель-из-памяти_order_consume-in-cpp11/

atomic<int*> Guard(nullptr);
int Payload = 0;

поток1:

  Payload = 42;
    Guard.store(&Payload, memory_order_release);

поток2:

g = Guard.load(memory_order_consume);
if (g != nullptr)
    p = *g;

ВОПРОС: Я узнал, что зависимость от данных предотвращает изменение порядка связанных инструкций. Но я думаю, что это очевидно для обеспечения правильности результатов выполнения. Неважно, существует ли семантика comsume-release или нет. Так что мне интересно, что comsume-релиз действительно делает. О, может быть, он использует зависимости данных, чтобы предотвратить изменение порядка инструкций, обеспечивая при этом видимость полезной нагрузки?

Так

Можно ли получить тот же правильный результат, используя memory_order_relaxed, если я переупорядочу эту инструкцию 1.preventing 2.обеспечение видимости неатомарной переменной полезной нагрузки :

atomic<int*> Guard(nullptr);
volatile int Payload = 0;   // 1.Payload is volatile now

// 2.Payload.assign and Guard.store in order for data dependency
Payload = 42;               
Guard.store(&Payload, memory_order_release);

// 3.data Dependency make w/r of g/p in order
g = Guard.load(memory_order_relaxed);  
if (g != nullptr)
    p = *g;      // 4. For 1,2,3 there are no reorder, and here, volatile Payload make the value of 42 is visable.

Дополнительный контент (из-за ответа Sneftel):

1.Полезная нагрузка = 42; volatile делает W / R полезной нагрузки в / из основной памяти, но не в / из кеша. Таким образом, 42 будет записывать в память.

2.Guard.store(&Payload, для записи можно использовать любой флаг МО); Guard, как вы сказали, энергонезависим, но является атомарным

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

На самом деле, атомарность всегда потокобезопасна, независимо от объема памяти. заказ! Порядок памяти не для атомарных -> это для не атомарных данные.

Таким образом, после выполнения Guard.store Guard.load (с любым флагом MO, который можно использовать для чтения) может правильно получить адрес Payload. А затем правильно получить 42 из памяти.

Над кодом:

1. нет эффекта переупорядочения для зависимости от данных.

2. нет эффекта кеша для энергозависимой полезной нагрузки

3. нет проблем с потокобезопасностью для atomic Guard

Могу ли я получить правильное значение - 42?

Вернемся к основному вопросу

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

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

Я думаю, что цепочки зависимостей данных на уровне исходного кода C++ предотвращают естественное изменение порядка инструкций. Так что же на самом деле делает memory_order_consume?

И могу ли я использовать memory_order_relaxed для достижения того же результата, что и вышеприведенный код?

Конец дополнительного контента

«Но я думаю, что это очевидно для обеспечения правильности результатов выполнения». Как же так? Как вы думаете, какие проблемы с корректностью в однопоточной программе могут возникнуть, если, например, записи в коде a=1; b=2 будут переупорядочены?

Sneftel 17.12.2020 08:47

Неважно, если a=1; b=2 переупорядочиваются. Но a=1; b=a были переупорядочены неправильно. Это то, что «очевидно для обеспечения правильности результатов выполнения», которое я хочу выразить.

breaker00 17.12.2020 09:09

Это не то, что делает ваш код (любая версия). Мне непонятно, почему вы считаете, что volatile+relaxed эквивалентно релизу. Квалификатор volatile не ограничивает чтение/запись энергонезависимыми объектами (такими как ваш atomic). Это не имеет ничего общего с написанием совместимого многопоточного кода на C++.

Sneftel 17.12.2020 09:19

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

David Haim 17.12.2020 09:40

@Sneftel спасибо за ответ. Я добавил некоторый контент для вопроса.

breaker00 17.12.2020 11:28

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

Peter Cordes 18.12.2020 08:02

volatile делает W/R полезной нагрузки в/из основной памяти, но не в/из кеша - нет. Это гарантирует, что сохранение выполнено вообще, а не сохраняет значение в регистрах на потом. Регистры не кэшируются; многих людей смущает фраза типа «значение, кэшированное в регистрах». Это один из способов для программного обеспечения использовать регистр для хранения значения переменной, которая не изменяется, но фактический кеш ЦП отличается (и согласован). Когда использовать volatile с многопоточностью? - никогда, но на практике имеет некоторые эффекты.

Peter Cordes 18.12.2020 08:11

Также предназначено для ссылки на Мифы, в которые верят программисты о кеше ЦП

Peter Cordes 18.12.2020 08:34
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
10
8
2 037
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Дело в том, что ответ не совсем правильный, так как есть пара нюансов.

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

Они читают «последнее записанное значение», но с «расслабленным» порядком памяти порядок инструкций может быть изменен.

Так что, если вы говорите написать DoSomething(); x = y.load(relaxed);, то после компиляции расслабленная нагрузка может быть упорядочена до DoSomething();. И если предположить, что процедура заняла довольно много времени, то значение x может сильно отличаться от последнего значения y.

С порядком памяти «потребление» перестановка инструкций запрещена, поэтому такой проблемы не возникнет.

Я не думаю, что вы прочитали мой ответ полностью, потому что я затронул тему казни ООО.

David Haim 17.12.2020 09:39

@DavidHaim, ну, это единственная цель «потребления» порядка памяти. Я проверил статью, на которую вы ссылались, не могу сказать наверняка, но использование слова «потребить», которое они предлагают, звучит очень неправильно. Чтение указателя g через consume никоим образом не гарантирует, что доступ к данным указателя будет выполнен правильно. Даже если ЦП может каким-то образом обеспечить загрузку зависимостей, в чем я сомневаюсь, компилятор все еще может облажаться, делая предположения. Возможно, причина, по которой дефакто consume работает, заключается в том, что составители компиляторов запутались с этой инструкцией и реализовали ее как aquire+release.

ALX23z 17.12.2020 10:11

Я не ОП. Я тот, кому вы сказали, что его ответ не совсем правильный (подразумевая, что я не упомянул о переупорядочении вещей, но я это сделал)

David Haim 17.12.2020 10:16

@DavidHaim нет, правда. Я только что прочитал цитату ОП. У вас длинный ответ ... я подозреваю, что OP не понял его должным образом, и ссылка, которую он разместил, неверна.

ALX23z 17.12.2020 12:32

@ ALX23z: да, компилятор может сломать что-то, если вы используете Release + Relaxed. Но если asm имеет зависимость 2-го адреса от первого результата загрузки, процессору на самом деле очень сложно нарушить причинно-следственную связь и каким-то образом узнать, откуда загружать, прежде чем он получит адрес для этой загрузки. Все (?) Современные ISA, кроме DEC Alpha, гарантируют порядок зависимостей на бумаге, поэтому это не является обязательным или удачным, что он работает на оборудовании, это гарантируется поставщиками ЦП. (Но, как я уже сказал, для них это в основном бесплатно для реализации на стандартной машине exec OoO; только независимая работа может быть переупорядочена.)

Peter Cordes 18.12.2020 08:25

Фактический механизм, с помощью которого некоторые модели процессоров DEC Alpha могут каким-то образом нарушать порядок причинно-следственной связи/зависимости, очень неясен, но является хорошим примером того, почему обычно небезопасно предполагать, что «никакая конструкция процессора никогда не сможет на самом деле нарушить это предположение, которое я хочу сделать». ". Для получения дополнительной информации см. Переупорядочивание зависимых нагрузок в ЦП , а для других комментариев см. цитату Линуса Торвальдса об альфа-процессорах: Порядок потребления памяти в C11. Прогнозирование значений нагрузки может его сломать, но никакие настоящие процессоры этого не делают (пока?).

Peter Cordes 18.12.2020 08:29

@PeterCordes «да, компилятор может сломать что-то, если вы используете Release + Relaxed». Я считаю, что это может сломать что-то даже с помощью «релиз + потребление». Скажем, вы пишете x=3 и не модифицируете, а затем читаете атомарный y с потреблением - в этот момент я полагаю, что компилятор может предположить, что x==3, поэтому загрузку даже не нужно планировать.

ALX23z 18.12.2020 09:39

@PeterCordes как насчет кэшированных данных? При «потреблении» ЦП не нужно синхронизировать кэш. Что, если другой поток/ядро изменил данные, которые ранее были кэшированы в этом потоке/ядре? Поскольку не было срабатывания соответствующей границы памяти, вы получаете неверные значения из кеша, не так ли?

ALX23z 18.12.2020 09:40

@ ALX23z Кэш всегда чистый / обновленный. Он должен быть в разумной арке, предназначенной для MT.

curiousguy 28.12.2020 21:26
  1. volatile не имеет ничего общего с многопоточностью в c/c++, его побочный эффект последовательной видимости возникает только в однопоточной программе и обычно используется только для того, чтобы сообщить компилятору, что это значение не следует оптимизировать. Это ОТЛИЧАЕТСЯ от Java/C#.

  2. выпуск/потребление полностью зависит от данных, и он может создать цепочку зависимостей (которую можно разорвать с помощью kill_dependency, чтобы избежать ненужных барьеров позже).

  3. выпуск/приобретение образует парные отношения synchronize-with/inter-thread happens-before.

В вашем случае release/acquire сформирует ожидаемые happens-before отношения. release/consume также будет работать, потому что *g зависит от g.

Но обратите внимание, что в текущих компиляторах consume рассматривается как синоним acquire, потому что его оказалось слишком сложно эффективно реализовать. см. другой ответ

Да, все эти пункты верны, но выпуск/потребление здесь безопасно (независимо от volatile), даже на старых компиляторах, которые не просто продвигают потребление для получения. (consume временно устарело до тех пор, пока комитет C++ не придумает лучшее потребление, которое можно практически реализовать полностью безопасно, но все же эффективно, и без заражения кода всех тегами [[carries_dependency]].) g является результатом потребления, которое увидело выпуск-хранилище, поэтому *g заказывается после загрузки g.

Peter Cordes 18.12.2020 08:17

(выпуск/расслабление небезопасно на бумаге, но на большинстве ISA «случайно» будет работать, потому что компилятор создаст asm, который имеет зависимость от данных, и все (?) ISA, кроме Alpha, гарантируют порядок зависимостей. C++11 : разница между memory_order_relaxed и memory_order_consume — делайте это только в рабочем коде, если понимаете ситуацию, и заботитесь только об ограниченном наборе компиляторов. их отсутствие), но заботятся только о gcc/clang, а не о ISO C)

Peter Cordes 18.12.2020 08:21

Да, вы правы, потребление действительно здесь. Пожалуйста, не стесняйтесь редактировать мой ответ, спасибо.

Harold 18.12.2020 10:59
Ответ принят как подходящий

Прежде всего, memory_order_consume временно не рекомендуется комитетом ISO C++, пока они не придумают что-то, что компиляторы действительно могут реализовать. Вот уже несколько лет компиляторы рассматривают consume как синоним acquire. См. раздел внизу этого ответа.

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

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

Не совсем «переупорядочивание инструкций», а переупорядочивание памяти. Как вы говорите, в этом случае достаточно здравомыслия и причинно-следственной связи, если аппаратное обеспечение обеспечивает порядок зависимостей. С++ переносим на машины, которые этого не делают. (например, DEC Alpha.)

Обычный способ получить видимость полезной нагрузки — через хранилище выпуска в модуле записи, получить нагрузку в считывателе, который видит значение из этого хранилища выпуска. https://preshing.com/20120913/acquire-and-release-semantics/. (Поэтому, конечно, повторное сохранение одного и того же значения в «ready_flag» или указателе не позволяет читателю понять, видит ли он новое или старое хранилище.)

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

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


У вас есть некоторые неправильные представления о том, что такое кеш ЦП и что делает volatile, о чем я прокомментировал под вопросом. Хранилище выпуска обеспечивает видимость в памяти более ранних неатомарных назначений.

(Кроме того, кеш является согласованным; он предоставляет всем ЦП общее представление памяти, с которым они могут согласиться. Регистры являются частными потоками и не являются согласованными, это то, что люди имеют в виду, когда говорят, что значение «кэшируется». Регистры не являются ЦП. кэш, но программное обеспечение может использовать их для хранения копии чего-либо из памяти. Когда использовать volatile с многопоточностью? - никогда, но это имеет некоторые эффекты в реальных процессорах, потому что у них когерентный кеш. Это плохой способ скатывайте сами mo_relaxed См. также https://software.rajivprab.com/2018/04/29/myths-programmers-believe-about-cpu-caches/)

На практике на реальных процессорах переупорядочивание памяти происходит локально внутри каждого ядра; сам кеш согласован и никогда не «рассинхронизируется». (Другие копии становятся недействительными до того, как хранилище станет общедоступным). Так что release просто нужно убедиться, что хранилища локальных процессоров становятся глобально видимыми (фиксируются в кеше L1d) в правильном порядке. ISO C++ не определяет такой уровень детализации, и гипотетически возможна реализация, которая работала бы совершенно по-другому.

Делать хранилище записи изменчивым на практике не имеет значения, потому что неатомарное присваивание, за которым следует хранилище выпуска, уже должно сделать все видимым для других потоков, которые могут выполнять загрузку и синхронизацию с этим хранилищем выпуска. Это не имеет значения на бумаге в чистом ISO C++, потому что это не позволяет избежать гонки данных UB.

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


Что на самом деле делает memory_order_consume?

Одна вещь, которую делает mo_consume, — убедиться, что компилятор использует барьерную инструкцию для реализаций, где базовое оборудование не обеспечивает упорядочение зависимостей естественным образом/бесплатно. На практике это означает только на DEC Alpha. Зависимое изменение порядка загрузки в ЦП / Порядок потребления памяти в C11

Ваш вопрос почти дублирует C++11: разница между memory_order_relaxed и memory_order_consume — см. там ответы на основную часть вашего вопроса о ошибочных попытках делать что-то с volatile и Relaxed. (Я в основном отвечаю из-за вопроса заголовка.)

Это также гарантирует, что компилятор использует барьер в какой-то момент, прежде чем выполнение перейдет к коду, который не знает о зависимости данных, которую несет это значение. (т. е. нет тега [[carries_dependency]] в функции arg в объявлении). Такой код может заменить x-x константой 0 и оптимизировать, потеряв зависимость от данных. Но код, который знает о зависимости, должен будет использовать что-то вроде инструкции sub r1, r1, r1, чтобы получить ноль с зависимостью данных.

Этого не может произойти для вашего варианта использования (где relaxed будет работать на практике на ISA, отличных от Alpha), но на бумаге дизайн mo_consume допускал все виды вещей, которые требовали бы генерации кода, отличного от того, что обычно делают компиляторы. . Это часть того, что настолько усложнило эффективную реализацию, что компиляторы просто повысили ее до mo_acquire.

Другая часть проблемы заключается в том, что код должен быть замусорен kill_dependency и/или [[carries_dependency]] повсюду, иначе вы все равно столкнетесь с барьером на границах функций. Эти проблемы привели к тому, что комитет ISO C++ временно отклонил consume.

  • C++11: разница между memory_order_relaxed и memory_order_consume
  • P0371R1: Временно откажитесь от memory_order_consume и других документов C++ wg21, связанных с этим, о том, почему потребление не рекомендуется.
  • Порядок использования памяти в C11 — подробнее об аппаратном механизме / гарантии, что consume предназначен для доступа к программному обеспечению. Exec в любом случае может только переупорядочить независимую работу, не запуская загрузку до того, как станет известен адрес загрузки, поэтому на большинстве процессоров принудительное упорядочение зависимостей происходит бесплатно в любом случае: только несколько моделей DEC Alpha могут нарушать причинно-следственную связь и эффективно загружать данные. до того, как у него был указатель, который дал ему адрес.

И кстати:

Пример кода безопасен с release + consume независимо от volatile. Это безопасно для большинства компиляторов и большинства ISA на практике с release store + relaxed load, хотя, конечно, ISO C++ ничего не говорит о правильности этого кода. Но с текущим состоянием компиляторов это хак, который делает какой-то код (например, RCU ядра Linux).

Если вам нужен такой уровень масштабирования на стороне чтения, вам придется работать за пределами того, что гарантирует ISO C++. Это означает, что ваш код должен будет делать предположения о том, как работают компиляторы (и что вы работаете на «обычной» ISA, а не на DEC Alpha), а это означает, что вам необходимо поддерживать некоторый набор компиляторов (и, возможно, ISA, хотя вокруг не так много многоядерных ISA). Ядро Linux заботится только о нескольких компиляторах (в основном, о последнем GCC, я думаю, также о clang) и ISA, для которых у них есть код ядра.

Искренне спасибо за ваше реле. Могу ли я просто подумать, что: некоторый случай кода или аппаратной реализации не гарантирует, что зависимость от данных может быть использована для решения проблемы переупорядочения. Следовательно, первоначальная цель разработки cosume — помочь компилятору сгенерировать правильный код без переупорядочивания через зависимость от данных без ограничений. Хотя добиться этого слишком сложно.

breaker00 19.12.2020 05:30

@breaker00: Нет, существование HW без гарантий упорядочения зависимостей — это не то, зачем нам нужен consume. Вам все равно это понадобится без этого, чтобы контролировать генерацию кода и убедиться, что компилятор не оптимизирует зависимость. (Правила С++ должны быть формальными и точными; что-то вроде «пока вы не делаете что-то, что компилятор может оптимизировать» недостаточно конкретно).

Peter Cordes 19.12.2020 05:53

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

Peter Cordes 19.12.2020 05:53

Точно так же оптимизация зависимости данных в ветвь разрешена для расслабления, например. для чего-то вроде int idx = x.load(relaxed); int *p = table[idx]; q = *p; с таблицей из 2 элементов: компилятор может просто перейти к 0 против 1 и выбрать один, потеряв зависимость. Таким образом, ISO C++ нуждается в каком-то способе запретить компиляторам делать это, сохраняя при этом полную гибкость оптимизации для кода, который не зависит от упорядочения зависимостей данных. Таким образом, mo_consume необходим в той или иной форме как часть формальной спецификации языка, чтобы избежать того, чтобы все носило зависимость и запрещало ветвление.

Peter Cordes 19.12.2020 05:57

Я исправил свои мысли об изначальной цели потребления. Это близко к праву? Если есть потребление, это означает, что я хочу, чтобы компилятор гарантировал правильность зависимости данных. Не оптимизируйте, чтобы удалить зависимость данных, которая мне нужна. И даже на такой платформе, как DEC Alpha, добавьте барьер, чтобы обеспечить такое же правильное отношение зависимости данных. Для других случаев без потребления это равносильно тому, что я говорю компилятору, что мне не так важна правильность зависимости данных, чтобы выполнить оптимизацию, которую вы считаете правильной.

breaker00 19.12.2020 09:00

@breaker00: Да, верно. ISO C++ должен определить правила о том, что является зависимостью данных, а что нет в C++, и это то, что компиляторы должны учитывать при создании asm с использованием результата, который содержит зависимость. (Напрямую или косвенно от потребляемой нагрузки.)

Peter Cordes 19.12.2020 09:02

Спасибо таким людям, как ты, которые не могут лечь спать. Чтобы люди, которые заблудились, могли лечь спать раньше.

breaker00 19.12.2020 09:20

Другие вопросы по теме