Это вопрос о формальные гарантии стандарта С++.
Стандарт указывает, что правила для атомарных переменных std::memory_order_relaxed допускают появление значений «из воздуха» / «неожиданно».
Но для неатомарных переменных может ли этот пример иметь UB? Возможно ли r1 == r2 == 42 в абстрактной машине C++? Ни одна переменная == 42 изначально не должна выполняться, поэтому вы ожидаете, что ни одно тело if не будет выполняться, что означает отсутствие записи в общие переменные.
// Global state
int x = 0, y = 0;
// Thread 1:
r1 = x;
if (r1 == 42) y = r1;
// Thread 2:
r2 = y;
if (r2 == 42) x = 42;
Приведенный выше пример адаптирован из стандарта, который явно говорит, что такое поведение разрешено спецификацией для атомарных объектов:
[Note: The requirements do allow r1 == r2 == 42 in the following example, with x and y initially zero:
// Thread 1: r1 = x.load(memory_order_relaxed); if (r1 == 42) y.store(r1, memory_order_relaxed); // Thread 2: r2 = y.load(memory_order_relaxed); if (r2 == 42) x.store(42, memory_order_relaxed);However, implementations should not allow such behavior. – end note]
Какая часть так называемой "модели памяти" защищает неатомарные объекты от этих взаимодействий, вызванных чтением значений из воздуха?
Когда существует состояние гонки было бы со значениями разные для x и y, что гарантирует, что чтение общей переменной (обычной, неатомарной) не сможет увидеть такие значения?
Могут ли невыполненные if тела создать самореализующиеся условия, ведущие к гонке данных?
Я даже не совсем понимаю, какие гарантии, по вашему мнению, вы должны получить. Формулировка запутанная и трудная для разбора. Если вы можете пояснить это, я могу написать вам ответ, объясняющий, почему вы не получаете их на практике.
std::atomic_thread_fence должен гарантировать порядок чтения и записи, но это ничего не меняет в гонке.
@Omnifarious В идеале гарантия того, что последовательность операций не может произойти, поэтому x и y сохраняют исходное нулевое значение;
Модель памяти C++ позволяет прогнозировать значение (что, насколько я знаю, не делает никакое реальное оборудование), поэтому сохранение y = r1; действительно может произойти до того, как результат загрузки r1 = x будет готов, даже если вы использовали std::atomic с mo_relaxed, чтобы сделать этот код свободным от гонки данных UB. . Некоторые модели знаменитого слабого DEC Alpha действительно могут нарушать причинно-следственную связь для данных, на которые указывают (mo_consume), но я не думаю, что это касается простой загрузки и последующего сохранения результата загрузки.
О, и в этом случае компилятор может легко доказать, что внутри if (r1 == 42) он может сделать y = 42; вместо y = r1; разрыва зависимости данных. Таким образом, обычная спекуляция ветвями может позволить сохраниться до загрузки на слабо упорядоченной ISA, такой как ARM или PowerPC. (Снова предполагая, что std::atomic с mo_relaxed, или что небезопасный C был в основном транслитерирован в asm с использованием простой загрузки/сохранения для какой-то конкретной ISA, где мы можем затем рассуждать о модели памяти этого оборудования.)
@PeterCordes Как выглядит «нарушение причинно-следственной связи для указанных данных (mo_consume)»?
Вроде int *p = atomic_load(a_pointer);int value = atomic_load(p); (но с mo_relaxed, т.е. обычными asm-загрузками). Даже если поток записи использует барьеры, чтобы убедиться, что данные, на которые указывает, были глобально видны перед сохранением указателя, сторона чтения все еще может изменить порядок и прочитать данные, на которые указывает, до чтения указателя (таким образом, в результате получается value = старое содержимое *p). См. также Порядок потребления памяти в C11. Также гарантии в kernel.org/doc/Документация/memory-barriers.txt
Обновление: мои предыдущие комментарии немного не по теме (хотя и не ошибочны). Спекуляция между потоками Переупорядочивание памяти а также необходимо, чтобы это произошло на практике. И поскольку ни одно из условий должен не принимается, я думаю, что это произойдет для неатомарных переменных, что нарушит правило «как если бы». Я опубликовал ответ на этот счет.
@curiousguy Я не думаю, что результат допустим. Это нарушает фундаментальную причинно-следственную связь. Отношение причинности не имеет ничего общего с какой-либо моделью памяти (будь то память языка или процессора). Это основная логика и основа дизайна языка программирования. Это фундаментальный контракт между человеком и компьютером. Любая модель памяти должна ей соответствовать. В противном случае это ошибка.
@Xiao-FengLi "не имеет ничего общего с какой-либо моделью памяти" ОК, тогда 1) указано формально? 2) как это взаимодействует с моделью памяти? 3) не может ли «слабая» модель памяти вызывать эффекты, которые, кажется, нарушают «причинность»?
@curiousguy причинно-следственная связь здесь относится к зависимости внутри потока между операциями одного потока, такой как зависимость данных (например, чтение после записи в одном и том же месте) и зависимость управления (например, операция в ветке). Они не могут быть нарушены какой-либо конструкцией процессора в зафиксированном результате (т. е. внешне видимом результате). Модель памяти касается только упорядочения операций с памятью среди мультипроцессоров, они никогда не должны нарушать внутрипотоковую зависимость, хотя слабая модель может допускать нарушение (или невидимость) причинно-следственной связи, происходящей в одном процессоре, в другом процессоре.
(Продолжение) В вашем фрагменте кода оба потока имеют (внутрипотоковую) зависимость данных (загрузка->проверка) и зависимость управления (проверка->сохранение), которые обеспечивают упорядоченность их соответствующих исполнений (в одном потоке). Это означает, что мы можем проверить вывод более поздней операции, чтобы определить, была ли выполнена более ранняя операция. Затем мы можем использовать простую логику, чтобы сделать вывод, что если оба r1 и r2 равны 42, должен быть цикл зависимости, который невозможен, если вы не удалите проверки условий, что по существу устраняет зависимость. Это не имеет никакого отношения к модели памяти, а зависит от данных внутри потока.
@Xiao-FengLi Это требование причинно-следственной связи кажется элементарным, но ясно ли оно написано в C++ std?
@curiousguy Причинность (или, точнее, зависимость здесь) определена в C++ std, но не так явно, потому что зависимость больше связана с терминологией микроархитектуры и компилятора. В спецификации языка это обычно определяется с использованием таких терминов, как «область имени», «оператор if» и т. д., чтобы определить операционную семантику. Семантика имплицитно удовлетворяет причинности. Например, управляющая зависимость, образованная оператором if, определена в тимсонг-cpp.github.io/cppwp/n3337/stmt.if: «Если условие дает истину, выполняется первое подоператор».
@ Xiao-FengLi Похоже, вы подразумеваете, что чтение внутри предложения then не может быть выполнено до того, как условие станет истинным.
@curiousguy Да, это требование семантики. При этом компилятор и процессор могут запланировать выполнение одной или нескольких операций ветки if до того, как условие if будет разрешено. Но независимо от того, как компилятор и процессор планируют операции, результат if-ветви не может быть зафиксирован (т. е. стать видимым для программы) до того, как будет разрешено if-условие. Следует различать требования семантики и детали реализации. Один из них — спецификация языка, другой — то, как компилятор и процессор реализуют спецификацию языка.
@Xiao-FengLi Итак, в if (a.load(relaxed)) { r = b.load(relaxed); acquire_fence(); c.store(r,relaxed); } значение b не может быть загружено до значения a, или если это так, чтение должно быть отменено, если a равно true, чтобы неправильное значение в r не записывалось в c?
@curiousguy Компилятор/процессор может загрузить b до того, как значение a будет разрешено, потому что и a, и b являются расслабленными атомами. Но значение b не может быть записано в r до того, как будет выбрана истинная ветвь. То есть значение b загружено спекулятивно. Сама загрузка бесполезна - в этом контексте. На самом деле с нестрогим порядком значение b может быть загружено даже раньше значения a, но все же оно не может быть сохранено в r. Если перед загрузкой fence есть b, то он не может быть загружен до загрузки a, но именно здесь срабатывает модель памяти.
@curiousguy То, что вы описали, в значительной степени верно, но «загрузка b» — это не то же самое, что «запись r», потому что r виден программе. Запись в r означает, что результат спекулятивной загрузки b зафиксирован. Компилятор/процессор может временно и внутри системы записать значение b, но не r.
@Xiao-FengLi Даже при расслабленных нагрузках?
@curiousguy Не могли бы вы немного уточнить свой вопрос? Я не знаю, какое из моих утверждений вы имеете в виду.
@curiousguy: обновил мой ответ дополнительными доказательствами из более поздних стандартов о том, что значения из воздуха предназначены для запрещения, но формализм не может этого сделать, поэтому они добавили примечание. Более поздние стандарты формулируют это более четко. Ваш вопрос сформулирован запутанно, потому что ваши примеры на самом деле не содержат гонки данных. Так бы и было, если бы были разрешены значения из воздуха, поэтому вопрос в том, что защищает вас от значений из воздуха, а не от гонки данных. Мой ответ примиряет эту заметку/пример mo_relaxed с разумным неатомарным поведением. Предлагаю принять.
@Xiao-FengLi "Компилятор/процессор может временно и внутри системы записать значение b куда-нибудь, но не r." Даже с использованием расслабленной нагрузки ЦП не может этого сделать?
@PeterCordes Пример содержит гонку, если ... он содержит гонку, что позволяет читать другие значения, кроме того, что было в прошлом. Также я не думаю, что существует общепринятое определение «ценностей из воздуха» (хотя некоторые вещи повсеместно считаются «из воздуха»), как, может быть, и нет универсального определения того, что считается немецкая овчарка (или чихуахуа), но никто не называет чихуахуа тем, что другие называют немецкой овчаркой.
Все, что менее очевидно «из воздуха», может быть потенциальной гонкой данных UB из-за назначения, не синхронизированного с нагрузкой. Поэтому я не думаю, что был бы случай, когда вы могли бы сказать, что результат был из-за значения из воздуха, и что UB гонки данных определенно не произошло. (Я предполагаю, что некоторые фактические сохранения объектов будут необходимы для «основного» прогнозирования значений, а также для взаимно подтверждающих предположений между потоками.)
Говоря об определениях, для этого на процессорах требуется (я думаю) взаимно подтверждающее предположение между потоками, что-то вроде того, к чему мы привыкли. Это довольно четкое разграничение.
@curiousguy Запись в r здесь означает вызвать эффект в системе, который программа может увидеть и использовать. Независимо от того, насколько расслаблена модель памяти, значение new r никогда не должно быть видно программе до тех пор, пока условие if не будет разрешено. Это не имеет никакого отношения к модели памяти, о чем свидетельствует тот факт, что приведенный вами код является просто однопоточным. Очень легко смешать эти две концепции (зависимость и порядок памяти). OTOH, если мы действительно хотим сказать, что r может быть записано до того, как условие if будет разрешено, это нормально - до тех пор, пока значение r не видно/не используется программой.
Я значительно перефразировал ваш вопрос, чтобы избежать путаницы, которая привела к существующим ответам и голосам по ним. В то время мне они тоже казались правильными. Только после внимательного изучения примера стало ясно, что это не было явным нарушением.
@PeterCordes «компилятор может легко доказать, что внутри if (r1 == 42) он может сделать y = 42; вместо y = r1; нарушение зависимости от данных. Таким образом, обычная спекуляция ветвями может привести к тому, что сохранение произойдет до загрузки на слабо упорядоченной ISA.», но нельзя делать спекулятивные хранилища видимыми для других потоков? r1 = x и y = 42 может выйти из строя казнен, но никогда не выйдет из строя выходить на пенсию, даже на слабой ISA? Ссылка здесь
@DanielNitzan: это правильно, но это по-прежнему означает, что значение хранилища не упорядочено по зависимостям после загрузки. Загрузки взаимодействуют с глобальным состоянием при выполнении, а не при удалении. (Однако хранилища не могут стать глобально видимыми до выхода на пенсию, а ветвь load + должна выполняться до выхода на пенсию. Но если этот поток перезагружается y, он может увидеть свое собственное хранилище до того, как оно станет глобально видимым (переадресация хранилища). Так что, возможно, пример получше можно было бы состряпать, но я думаю тут есть какой-то эффект?
@PeterCordes, честно говоря, я написал предыдущий комментарий до того, как прочитал ваш ответ, особенно раздел о гипотетической архитектуре спекуляции между потоками SMT, которая действительно может воспроизвести такой сценарий (но нарушит гарантию отсутствия UB для DRF C++ ). Кроме того, похоже, возникла путаница вокруг исходного вопроса, которая привела к запутанным комментариям.
@DanielNitzan: правильно, да, перекрестное подтверждение спекуляций нарушило бы пример if (global_id == mine) shared_var = 123;, который, как я утверждал, является законным в моем ответе. Если реализации не обходят это, используя барьеры против спекуляций каждый раз, когда они загружают и/или сохраняют неатомарные переменные, при этом только mo_relaxed выявляет лежащее в основе DeathStation 9000 ужасное поведение оборудования. Гипотетические реализации вовсе не обязательно должны быть эффективными, не говоря уже о коммерческой жизнеспособности. :P Не то, чтобы нам нужно было изобретать его, поскольку мы не хочу не допускаем существования подобного, а ISO C++ говорит, что этого не должно быть :/
@PeterCordes LOL





When a race condition potentially exists, what guarantees that a read of a shared variable (normal, non atomic) cannot see a write
Нет такой гарантии.
Когда существует состояние гонки, поведение программы не определено:
[intro.races]
Two actions are potentially concurrent if
- they are performed by different threads, or
- they are unsequenced, at least one is performed by a signal handler, and they are not both performed by the same signal handler invocation.
The execution of a program contains a data race if it contains two potentially concurrent conflicting actions, at least one of which is not atomic, and neither happens before the other, except for the special case for signal handlers described below. Any such data race results in undefined behavior. ...
особый случай не очень относится к вопросу, но я включу его для полноты картины:
Two accesses to the same object of type
volatile std::sig_atomic_tdo not result in a data race if both occur in the same thread, even if one or more occurs in a signal handler. ...
Этот особый случай в основном унаследован от C90 с обновленным языком.
@Omnifarious и практически единственный портативный способ связи между обработчиком сигналов и остальной частью программы.
Означает ли это, что почти все программы на самом деле сломаны?
@curiousguy Большинство многопоточных программ используют мьютексы или другие примитивы синхронизации (или типы std::atomic) для защиты общих данных. Если нет, то да, ваша программа сломана.
Программы @MilesBudnek MT обычно используют мьютексы для кода, подобного тому, что указан в вопросе?
@curiousguy - Если x и y действительно являются одним и тем же фрагментом памяти, к которому обращаются более чем один поток, то часто они будут, да. Некоторый очень тщательно написанный код для структур данных без блокировок будет использовать несколько атомарных переменных очень специфическими способами без использования мьютексов. Но это очень сложный код, который нужно написать и исправить. В этом конкретном случае, если вас больше всего беспокоит то, что если и x, и y являются 0 до того, как любой поток войдет, они оба останутся 0, вы, вероятно, могли бы просто использовать атомарные и более ограниченные порядки памяти.
Незначительное примечание: гонки данных и условия гонки не одно и то же. Гонки данных — это неопределенное поведение, а условия гонки — нет. В случае состояния гонки порядок выполнения конкретных команд не указан (что приводит к (потенциально) разным результатам в разных запусках), но поведение действительно определено.
Проголосовали за то, что не ответили на часть вопроса, показанного в примерах, только за неуклюжую формулировку. Если вы хотите отменить откат Натана, чтобы повторно добавить мое редактирование, или найти менее неуклюжий способ внесения того же изменения, я был бы рад отменить отрицательный голос. Ни один из примеров не содержит UB гонки данных, наиболее очевидно, что второй пример использует atomic<T>
What part of the so called "memory model" protects non atomic objects from these interactions caused by reads that see the interaction?
Никто. На самом деле вы получаете обратное, и стандарт явно называет это неопределенным поведением. В [intro.races]\21 у нас есть
The execution of a program contains a data race if it contains two potentially concurrent conflicting actions, at least one of which is not atomic, and neither happens before the other, except for the special case for signal handlers described below. Any such data race results in undefined behavior.
который охватывает ваш второй пример.
Правило состоит в том, что если у вас есть общие данные в нескольких потоках, и хотя бы один из этих потоков записывает эти общие данные, вам нужна синхронизация. Без этого у вас есть гонка данных и неопределенное поведение. Обратите внимание, что volatile не является допустимым механизмом синхронизации. Вам нужны переменные atomics/mutexs/condition для защиты общего доступа.
Если это так, я не могу представить, чтобы какая-либо программа машинного перевода в реальном мире была правильной.
@curiousguy Пока вы используете последовательно согласованный режим, вы гарантированно будете иметь единый общий порядок вашего кода. Это предлагается C++, поэтому он отлично подходит для написания многопоточного кода, который на 100% переносим и гарантирован.
Как активировать этот "режим"?
@curiousguy - Ну, это совсем не так. Есть причина, по которой стандарт добавил в язык примитивы атомарности и синхронизации потоков. Если вы используете их, вы получаете гарантии, которые обычно не применяются вообще.
@curiousguy - Используйте memory_order_seq_cst вместо memory_order_relaxed.
@curiousguy Обычно просто используя значения по умолчанию. Например, если у вас есть std::atomic<int> и вы выполняете ++name_of_atomic_int в нескольких потоках, результат будет правильным, поскольку по умолчанию операторы последовательно согласованы.
@NathanOliver Для магазина вы можете использовать memory_order_seq_cst, но как насчет деструктора?
@curiousguy Два потока не будут пытаться уничтожить атом. Если они это сделают, у вас есть большая проблема. Это то же самое, как если бы два потока пытались уничтожить что-то еще.
@FrançoisAndrieux Если только один поток уничтожает объект, у вас нет проблемы, но если только один поток записывает в объект, у вас есть проблема?
@FrançoisAndrieux Но в моем вопросе в каждую переменную записывается только один поток.
@curiousguy - Я думаю, ты запутываешься, пытаясь обдумать какие-то сложные идеи. Вместо того, чтобы пытаться прийти к какому-то нисходящему пониманию вещей, попробуйте несколько очень конкретных примеров (в идеале код, который действительно может работать). Возможно, опубликуйте их на SO и спросите, каково ожидаемое поведение. Стройте свое понимание снизу вверх, пока оно не щелкнет.
@curiousguy Нет. Если у вас есть один поток, уничтожающий объект, все в порядке. Если у вас есть два потока, пытающихся уничтожить объект, это не нормально. Но это не проблема многопоточности. Это так же плохо, как если бы один поток дважды пытался уничтожить объект.
@curiousguy У вас не может быть объекта, совместно используемого несколькими потоками, который будет уничтожен обоими потоками. Если это глобальный объект, он уничтожается в конце программы. Если он передается по ссылке, то он уничтожается сайтом вызова, а не потоками. Если вы передадите shared_ptr потокам, то, как только они закончатся, они уничтожат свою собственную копию shared_ptr, а shared_ptr использует атомарный подсчет ссылок, чтобы определить, когда он должен уничтожить объект, на который указывает.
@FrançoisAndrieux Если у вас есть один поток, пытающийся уничтожить объект, а другой может использовать (не уничтожать) объект, то какие меры предосторожности следует использовать?
@curiousguy Вот для чего нужен std::shared_ptr. Он позволяет двум или более вещам совместно владеть и уничтожает указанный объект только после того, как все shared_ptr будут уничтожены.
@NathanOliver Да, но это, по сути, ставит проблему перед разработчиком. Как код C++ интеллектуального указателя может обеспечить правильное последовательное поведение? Если все операции над атомарным объектом должны быть memory_order_seq_cst, то как std::shared_ptr обеспечивает такое же уничтожение? Разрушение memory_order_seq_cst?
@любопытный парень Да. Стандарт C++ требует, чтобы shared_ptr поступал правильно. Вам не нужно беспокоиться об этом.
@NathanOliver Мне нужно беспокоиться, чтобы понять, как написать правильный код. Нужно ли использовать memory_order_seq_cst в деструкторе объекта, которым управляет std::shared_ptr?
@curiousguy Нет. shared_ptr сделает все за вас за кулисами. Он использует атомарный счетчик ссылок, чтобы отслеживать количество экземпляров man. Деструктор проверяет счетчик ссылок и, если он больше единицы, просто атомарно уменьшает его на единицу. Если счетчик ссылок равен единице, то деструктор знает, что это единственный объект, которому принадлежит указатель, поэтому он удаляет удерживаемый указатель.
@curiousguy Если вы действительно хотите понять многопоточный код на C++, я бы посоветовал приобрести книгу, посвященную этому, например Вот этот. Он может сделать работу по созданию прочного фундамента гораздо лучше, чем мы.
@NathanOliver "Пока вы используете последовательно согласованный режим, вы гарантированно будете иметь единый общий порядок вашего кода." Если вы когда-либо используете только атомарные методы, то да. Но тогда у вас не может быть даже полиморфизма! (Вызов ctor не является атомарным.)
Во втором примере (цитируется из стандарта ISO C++, я думаю, C++14) единственными общими переменными являются atomic<T>. Нет UB-гонки данных, и в целом несинхронизированный доступ к атомарным переменным - это, возможно, только логическая ошибка садового разнообразия, а не UB. Извините за беспорядок, в котором мои правки оставили ваш ответ (я сделал первый, прежде чем понял, что вопрос касается значений из воздуха), но это состояние еще хуже, чем состояние после моего последнего редактирования, ИМО. Придется понизить голос за неверные утверждения об этих конкретных примерах.
Примечание: Конкретные примеры, которые я здесь привожу, явно не точны. Я предположил, что оптимизатор может быть несколько более агрессивным, чем это, по-видимому, разрешено. Есть отличная дискуссия по этому поводу в комментариях. Мне придется исследовать это дальше, но я хотел бы оставить эту заметку здесь в качестве предупреждения.
Другие люди дали вам ответы, цитируя соответствующие части стандарта, в которых прямо говорится, что гарантия, по вашему мнению, существует, не существует. Похоже, вы интерпретируете часть стандарта, в которой говорится, что определенное странное поведение разрешено для атомарных объектов, если вы используете memory_order_relaxed как означающее, что это поведение не разрешено для неатомарных объектов. Это скачок вывода, который явно рассматривается другими частями стандарта, которые объявляют поведение неопределенным для неатомарных объектов.
С практической точки зрения, вот порядок событий, которые могут произойти в потоке 1, что было бы совершенно разумно, но привело бы к поведению, которое вы считаете запрещенным, даже если аппаратное обеспечение гарантировало, что весь доступ к памяти был полностью сериализован между процессорами. Имейте в виду, что стандарт должен учитывать не только поведение оборудования, но и поведение оптимизаторов, которые часто агрессивно переупорядочивают и переписывают код.
Оптимизатор может переписать поток 1, чтобы он выглядел следующим образом:
old_y = y; // old_y is a hidden variable (perhaps a register) created by the optimizer
y = 42;
if (x != 42) y = old_y;
У оптимизатора могут быть вполне разумные причины для этого. Например, он может решить, что запись 42 в y гораздо более вероятна, чем нет, и по причинам зависимости конвейер может работать намного лучше, если сохранение в y произойдет раньше, чем позже.
Правило состоит в том, что видимый результат должен выглядеть как будто, а код, который вы написали, — это то, что было выполнено. Но нет требования, чтобы код, который вы пишете, имел хоть какое-то сходство с тем, что на самом деле сказано делать процессору.
Атомарные переменные накладывают ограничения на способность компилятора переписывать код, а также указывают компилятору выдавать специальные инструкции ЦП, которые налагают ограничения на способность ЦП переупорядочивать доступ к памяти. Ограничения, связанные с memory_order_relaxed, намного сильнее, чем обычно разрешено. Обычно компилятору разрешается полностью избавиться от любых ссылок на x и y, если они не являются атомарными.
Кроме того, если они являются атомарными, компилятор должен убедиться, что другие ЦП видят всю переменную либо с новым значением, либо со старым значением. Например, если переменная представляет собой 32-битный объект, который пересекает границу строки кэша, а модификация включает изменение битов по обе стороны границы строки кэша, один ЦП может увидеть значение переменной, которое никогда не записывается, потому что он видит только обновление битов на одной стороне границы строки кэша. Но это не разрешено для атомарных переменных, модифицированных с помощью memory_order_relaxed.
Вот почему гонки данных помечены стандартом как неопределенное поведение. Пространство возможных событий, которые могут произойти, вероятно, намного более дикое, чем может объяснить ваше воображение, и, безусловно, шире, чем может разумно охватить любой стандарт.
"Обычно компилятору разрешается полностью избавиться от любой ссылки на x и y вообще." как так? "если бы они не были атомарными." Что это такое, компилятор не может "полностью избавиться" от атомарных переменных?
@curiousguy - Нет, компилятор не может исключить операции над атомарными переменными из существования. Хотя, если атомарные переменные являются локальными и компилятор может доказать, что они никогда не используются совместно, он может полностью избавиться от переменных. Неатомарные переменные, которые являются общими (например, они глобальные, или вы дали указатель на них кому-то или чему-то), также не могут быть удалены. Но любой доступ к ним может быть удален до тех пор, пока конечный результат выглядит как как будто, код, который вы написали, является тем, что было выполнено. Это неверно для атомарных переменных. Все обращения, которые вы указываете в коде, должны появиться в сборке.
@curiousguy - Конечно, это правило «не исключать доступ» также верно для volatile переменных. Но атомарные переменные имеют еще несколько ограничений (например, граничное ограничение строки кэша), которых нет у volatile-переменных.
"Все обращения, которые вы указываете в коде, должны появиться в сборке." Почему именно?
@curiousguy - Думаю, на самом деле я ошибаюсь. Это является верно для volatile. Там такое правило. С атомом сложнее. Используемые вами порядки памяти определяют, какой именно доступ можно исключить. Если вы используете последовательный доступ, ни один из них не может быть. Семантика диктует это. Это потому, что memory_order_seq_cst требует, чтобы написанное вами значение было видно всем другим потокам, которые могут читать с memory_order_acquire или выше. И точно так же любое чтение с memory_order_seq_cst должно подобрать любое значение, записанное другим потоком с memory_order_release или выше.
@curiousguy - Для memory_order_relaxed доступы могут быть свободно переупорядочены или даже исключены. Единственное требование состоит в том, что любое фактическое чтение или запись должно записывать или читать все значение и не может читать или записывать частичное значение.
@curiousguy и Omni: компиляторы ISO C++ 11/14/17, как написано позволяет, для оптимизации нескольких последовательных атомарных хранилищ, но текущие компиляторы выбрать не делать этого (обращаясь с ними как volatile atomic), потому что нет очевидного способа сделать это без возможности делать то, что мы хотим, не, например сворачивать все магазины, чтобы обновить счетчик индикатора выполнения в один в конце. См. Почему компиляторы не объединяют избыточные записи std::atomic? для получения подробной информации о текущих компиляторах и обсуждениях / идеях стандартов.
@curiousguy — Этот вопрос может показаться вам интересным — stackoverflow.com/questions/12346487/…
Предлагаемый вами механизм (выполнение y=42, а затем условное возвращение к старому значению) в целом не является законным. Компиляторы не могут изобретать записи по путям, которые (в абстрактной машине C++) вообще не пишут y. Это создало бы проблемы с корректностью, если бы оказалось, что этот поток не должен был писать y, а другой поток писал y одновременно. (@curiousguy мы говорили об этой проблеме в комментариях к другой поток). IDK, если предсказание значений для нагрузок + другие сумасшедшие вещи могут позволить это на гипотетической ISA.
Обновление: опубликовал ответ. Я не думаю, что r1=r2=42 разрешено для неатомарных переменных. В абстрактной машине C++ нет UB: при заданных начальных значениях x и y ни один поток не записывает x или y. Код, который не записывает переменную, не может мешать тому, что другие потоки читают из него, даже если это условно может быть.
@PeterCordes Да. Этот ответ неверен в том смысле, что он предоставляет программе возможное «внутреннее» спекулятивное состояние работы процессора и предполагает, что компилятор может сделать то же самое. Внутреннее состояние процессора должно быть скрыто от результата выполнения программы и никогда не должно быть видно, не говоря уже о том, чтобы быть «реализованным» компилятором. Если они это делают, это ошибка, независимо от того, вызвана ли она конструкцией процессора или реализацией компилятора.
@Omnifarious: в некоторых случаях допускается спекуляция программным обеспечением является. например если y уже было безоговорочно записано с тем или иным значением, например. y = condition ? a : b; может быть скомпилировано в y=b;, а затем в условное хранилище b, если компилятор захочет. Но, как я прокомментировал ранее, изобретать записи в объекты, которые не записаны по правильному пути выполнения, недопустимо.
В тексте вашего вопроса, похоже, отсутствует суть примера и значения из воздуха. Ваш пример не содержит UB гонки данных. (Возможно, если x или y были установлены на 42 до запуска этих потоков, и в этом случае все ставки сняты, и применяются другие ответы, ссылающиеся на гонку данных UB.)
Защиты от реальных гонок данных нет, только от выдуманных значений.
Я думаю, вы действительно спрашиваете, как согласовать этот mo_relaxed пример с разумным и четко определенным поведением для неатомарных переменных. Вот что охватывает этот ответ.
mo_relaxed, нет предупреждает вас о реальном возможном влиянии на некоторые реализации.Этот пробел не распространяется (я думаю) на неатомарные объекты, Только на mo_relaxed.
Говорят Однако реализации не должны допускать такого поведения. - конец примечания]. Судя по всему, комитет по стандартам не смог найти способ формализовать это требование, поэтому пока это просто примечание, но оно не должно быть необязательным.
Понятно, что, хотя это и не является строго нормативным, стандарт C++ намеревается запрещает значения из воздуха для расслабленного атома (и вообще я предполагаю). Более позднее обсуждение стандартов, т.е. p0668r5 2018: пересмотр модели памяти C++ (который не «исправляет» это, это несвязанное изменение) включает сочные боковые узлы, такие как:
We still do not have an acceptable way to make our informal (since C++14) prohibition of out-of-thin-air results precise. The primary practical effect of that is that formal verification of C++ programs using relaxed atomics remains unfeasible. The above paper suggests a solution similar to http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3710.html . We continue to ignore the problem here ...
Так что да, нормативные части стандарта явно слабее для расслабленного_атомного, чем для неатомарного. Это, к сожалению, побочный эффект того, как они определяют правила.
Насколько я знаю, никакие реализации не могут создавать значения из воздуха в реальной жизни.
Более поздние версии стандартной фразы неофициальная рекомендация более четко, например. в текущем черновике: https://timsong-cpp.github.io/cppwp/atomics.order#8
- Implementations should ensure that no “out-of-thin-air” values are computed that circularly depend on their own computation.
...
[ Note: The recommendation [of 8.] similarly disallows
r1 == r2 == 42in the following example, with x and y again initially zero:// Thread 1: r1 = x.load(memory_order::relaxed); if (r1 == 42) y.store(42, memory_order::relaxed); // Thread 2: r2 = y.load(memory_order::relaxed); if (r2 == 42) x.store(42, memory_order::relaxed);— end note ]
(Эта остальная часть ответа была написана до того, как я был уверен, что стандартный предназначена также запрещает это для mo_relaxed.)
Я почти уверен, что абстрактная машина C++ допускает нетr1 == r2 == 42.
Всякий возможный порядок операций в операциях абстрактной машины C++ приводит к r1=r2=0 без UB, даже без синхронизации. Поэтому в программе нет UB и любой ненулевой результат нарушил бы правило "как если бы".
Формально ISO C++ позволяет реализации реализовывать функции/программы любым способом, который дает тот же результат, что и абстрактная машина C++. Для многопоточного кода реализация может выбрать один возможный порядок абстрактной машины и решить, что всегда происходит именно такой порядок. (например, при переупорядочивании нестрогих атомарных хранилищ при компиляции в asm для строго упорядоченной ISA. Стандарт в том виде, в котором он написан, даже позволяет объединять атомарные хранилища, но компиляторы предпочитают не). Но результатом программы всегда должно быть то, что произвела абстрактная машина мог.. (Только глава Atomics вводит возможность того, что один поток наблюдает за действиями другого потока без мьютексов. В противном случае это невозможно без UB-гонки данных).
Я думаю, что другие ответы недостаточно внимательно рассмотрели это. (И я тоже, когда это было впервые опубликовано). Код, который не выполняется, не вызывает UB (включая UB гонки данных) и компиляторам не разрешено изобретать записи для объектов. (За исключением путей кода, которые уже записывают их безусловно, например y = (x==42) ? 42 : y;, которые было бы явно создают UB гонки данных.)
Для любого неатомарного объекта, если фактически не записывает его, другие потоки также могут его читать, независимо от кода внутри невыполненных блоков if. Стандарт допускает это и не позволяет переменной внезапно считываться как другое значение, если абстрактная машина не записала его. (А для объектов, которые мы даже не читаем, таких как соседние элементы массива, их может даже записывать другой поток.)
Поэтому мы не можем сделать ничего, что позволило бы другому потоку временно увидеть другое значение для объекта или вмешаться в его запись. Изобретение записи в неатомарные объекты в основном всегда является ошибкой компилятора; это хорошо известно и общепризнано, потому что может сломать код, не содержащий UB (и это произошло на практике для нескольких случаев ошибок компилятора, которые его создали, например, IA-64 GCC, я думаю, что такая ошибка была в один раз). момент, который сломал ядро Linux). IIRC, Херб Саттер упомянул такие ошибки в части 1 или 2 своего выступления atomic<> Weapons: модель памяти C++ и современное оборудование», сказав, что это уже обычно считалось ошибкой компилятора до C++ 11, но C++ 11 систематизировал это и упростил уверенность.
Или другой недавний пример с ICC для x86: Сбой с icc: может ли компилятор изобрести записи там, где их не было в абстрактной машине?
В абстрактной машине С++, выполнение не может достичь ни y = r1;, ни x = r2;, независимо от последовательности или одновременности нагрузок для условий ветвления. x и y оба читаются как 0, и ни один из потоков никогда их не записывает.
Синхронизация не требуется, чтобы избежать UB, потому что никакой порядок операций абстрактной машины не приводит к гонке данных. Стандарт ISO C++ ничего не говорит о спекулятивном выполнении или о том, что происходит, когда неправильное предположение достигает кода. Это потому, что спекуляция — это особенность реальных реализаций, нет абстрактной машины. Реализации (поставщики аппаратного обеспечения и составители компиляторов) должны обеспечить соблюдение правила «как если бы».
В C++ разрешено писать такой код, как if (global_id == mine) shared_var = 123; и выполнять его всеми потоками, если не более одного потока фактически выполняет оператор shared_var = 123;. (И пока существует синхронизация, чтобы избежать гонки данных на неатомных int global_id). Если такие вещи, как это, сломаются, это будет хаос. Например, вы могли бы сделать неверные выводы, такие как переупорядочивание атомарных операций в C++
Наблюдение за тем, что отказ от записи не произошел, не является гонкой данных UB.
Это также не UB для запуска if (i<SIZE) return arr[i];, потому что доступ к массиву происходит только в том случае, если i находится в пределах.
Я думаю, что "ни с того ни с сего" примечание об изобретении ценности Только относится к расслабленной атомарности,, по-видимому, как особое предостережение для них в главе «Атомика». (И даже тогда, AFAIK, это не может произойти ни в каких реальных реализациях C++, уж точно не в основных. На данный момент реализации не должны принимать какие-либо специальные меры, чтобы убедиться, что это не может произойти для неатомарных переменных. )
Я не знаю ни одного подобного языка за пределами главы стандарта об атомарности, который позволяет реализации позволять значениям появляться из ниоткуда, как это.
Я не вижу разумного способа утверждать, что абстрактная машина C++ вызывает UB в любой момент при выполнении этого, но наблюдение r1 == r2 == 42 будет означать, что произошло несинхронизированное чтение + запись, но это гонка данных UB. Если это может случиться, может ли реализация изобрести UB из-за спекулятивного выполнения (или по какой-то другой причине)? Чтобы стандарт C++ вообще можно было использовать, ответ должен быть "нет".
Для расслабленных атомов изобретение 42 из ниоткуда не означало бы, что UB произошло; возможно, поэтому стандарт говорит, что это разрешено правилами? Насколько я знаю, за пределами глава стандарта Atomics ничего этого не позволяет.
(Никто этого не хочет, надеюсь, все согласятся с тем, что было бы плохой идеей создавать подобное оборудование. Маловероятно, что спекуляция связывания между логическими ядрами когда-либо будет стоить того недостатка, что придется откатывать все ядра, когда одно из них обнаруживает неверное предсказание или другое. неправильное предположение.)
Чтобы 42 было возможно, поток 1 должен видеть спекулятивное хранилище потока 2 и хранилище из потока 1 должны быть видны при загрузке потока 2. (Подтверждая эту спекуляцию ветвления как хорошую, позволяя этому пути выполнения стать реальным путем, который был фактически выбран.)
то есть спекуляция между потоками: возможно на текущем аппаратном обеспечении, если они работают на одном ядре только с облегченным переключением контекста, например. сопрограммы или зеленые нити.
Но на текущем HW переупорядочивание памяти между потоками в этом случае невозможно. Неупорядоченное выполнение кода на том же ядре создает иллюзию того, что все происходит в порядке программы. Чтобы переупорядочить память между потоками, они должны работать на разных ядрах.
Таким образом, нам потребуется конструкция, которая объединит спекуляции между двумя логическими ядрами. Никто этого не делает, потому что это означает, что при обнаружении неверного предсказания является необходимо откатить большее количество состояний. Но это гипотетически возможно. Например, ядро OoO SMT, которое позволяет осуществлять переадресацию хранилища между своими логическими ядрами даже до того, как они уйдут из неисправного ядра (т. е. станут неспекулятивными).
PowerPC позволяет пересылать хранилища между логическими ядрами для хранилищ на пенсии, что означает, что потоки могут не соглашаться с глобальным порядком хранилищ. Но ожидание, пока они «выйдут на пенсию» (то есть уйдут на пенсию) и станут неспекулятивными, означает, что это не связывает воедино спекуляции на отдельных логических ядрах. Таким образом, когда один восстанавливается после промаха ветки, другие могут занять серверную часть. Если бы им всем пришлось выполнять откат при неверном прогнозе на каком-либо логическом ядре, это лишило бы значительную часть преимуществ SMT.
Некоторое время я думал, что нашел порядок, который приводит к этому на одном ядре реальных слабоупорядоченных процессоров (с переключением контекста пользовательского пространства между потоками), но последнее хранилище шагов не может перейти к первому шагу load, потому что это порядок программы, и OoO exec сохраняет его.
T2: r2 = y; зависает (например, промах кеша)
T2: предсказание ветвления предсказывает, что r2 == 42 будет истинным. ( x = 42 должен работать.
T2: x = 42 бежит. (Все еще спекулятивно; r2 = yhasn't obtained a value yet so ther2 == 42` сравнение/ветвь все еще ждет подтверждения этого предположения).
происходит переключение контекста на поток 1 без, который откатывает ЦП в состояние вывода из эксплуатации или иным образом ожидает, пока предположение будет подтверждено как правильное или будет обнаружено как неправильное предположение.
Эта часть не произойдет в реальных реализациях C++, если они не используют модель потока M:N, а не более распространенный поток C++ 1:1 к потоку ОС. Реальные ЦП не переименовывают уровень привилегий: они не принимают прерывания или иным образом не входят в ядро со спекулятивными инструкциями в полете, которым может потребоваться откат и повторный вход в режим ядра из другого архитектурного состояния.
T1: r1 = x; берет свою стоимость из спекулятивного магазина x = 42
T1: r1 == 42 оказывается правдой. (Здесь также происходит спекуляция ветвления, фактически не ожидая завершения переадресации хранилища. Но на этом пути выполнения, где действительно произошло x = 42, это условие ветвления будет выполнено и подтвердит прогноз).
T1: y = 42 бежит.
все это было на одном и том же ядре ЦП, так что это хранилище y=42 после загрузки r2=y в программном порядке; он не может дать этой нагрузке 42, чтобы предположение r2==42 подтвердилось. Таким образом, этот возможный порядок в конце концов не демонстрирует это в действии. Вот почему потоки должны выполняться на отдельных ядрах с межпотоковыми спекуляциями, чтобы такие эффекты были возможны.
Обратите внимание, что x = 42 не зависит от данных r2, поэтому для этого не требуется прогнозирование значения. И y=r1 в любом случае находится внутри if (r1 == 42), поэтому компилятор может оптимизировать до y=42, если он хочет, нарушая зависимость данных в другом потоке и делая вещи симметричными.
Обратите внимание, что аргументы о зеленых потоках или другом переключении контекста на одном ядре на самом деле не имеют значения: нам нужны отдельные ядра для переупорядочивания памяти.
Я прокомментировал ранее, что я думал, что это может включать в себя прогнозирование стоимости. Модель памяти стандарта ISO C++, безусловно, достаточно слаба, чтобы допустить безумное «переупорядочивание», которое может создать прогнозирование значений, но для этого переупорядочивания это не обязательно. y=r1 можно оптимизировать до y=42, а исходный код в любом случае включает x=42, поэтому данные этого хранилища не зависят от загрузки r2=y. Спекулятивные запасы 42 легко возможны без прогнозирования стоимости. (Проблема в том, чтобы заставить другой поток их увидеть!)
Спекуляция из-за предсказания ветвления вместо предсказания значения имеет тот же эффект здесь. И в обоих случаях нагрузки должны в конечном итоге увидеть 42, чтобы подтвердить правильность предположения.
Предсказание значения даже не помогает сделать это переупорядочение более правдоподобным. Нам по-прежнему требуется переупорядочивание памяти между потоками а также для двух спекулятивных хранилищ, чтобы подтвердить друг друга и запустить себя в существование.
ISO C++ разрешает это для ослабленных атомов, но AFAICT запрещает эти неатомарные переменные. Я не уверен, что точно вижу, что в стандарте делает разрешает расслабленно-атомарный случай в ISO C++, кроме примечания, в котором говорится, что это явно не запрещено. Если бы был какой-то другой код, который что-то делал с x или y, то возможно, но я думаю, что мой аргумент делает применим и к расслабленному атомарному случаю. Никакой путь через источник в абстрактной машине C++ не может его создать.
Как я уже сказал, на практике это невозможно на любом реальном оборудовании (в ассемблере) или на С++ на любой реальной реализации С++. Это скорее интересный мысленный эксперимент с безумными последствиями очень слабых правил упорядочения, таких как расслабленно-атомарные правила C++. (Правила упорядочения Те не запрещают это, но я думаю, что правило «как если» и остальная часть стандарта разрешают, если только не существует какого-либо положения, позволяющего расслабленным атомарным вычислениям читать значение, которое было никогда фактически записано любым потоком.)
Если и существует такое правило, то только для ослабленных атомарных переменных, а не для неатомарных переменных. Data-race UB — это почти все, что стандарт должен сказать о неатомарных переменных и упорядочении памяти, но у нас этого нет.
Расслабленно-атомарные должны быть не более расслабленными, чем неатомарные. И несмотря ни на что, домыслы должны подтверждаться только неспекулятивным результатом, а не циклическим самодоказательством. Но ваш ответ в любом случае является хорошим упражнением для размышлений. :)
@Xiao-FengLi: «должно быть» - да, поэтому стандарт C++ говорит, что реализации должен не позволяют этого. Кроме того, почему разработчики настоящего HW никогда не создавали HW, которое могло бы это сделать. Да, это мысленное упражнение о том, какое безумие возможно, если правила слишком слабы, и я думаю, что слышал об этом в контексте архитектуры ЦП (вне C++). Как я сказал в ответе, правила упорядочения в главе Atomics могут разрешить это, но, возможно, не в сочетании с частями разное стандарта C++. Я не уверен, что это нужно упоминать как возможность в главе об атомной энергии.
@Xiao-FengLi: я нашел еще несколько доказательств того, что примечание не является нормативным Только, потому что они не смогли найти приемлемый способ его формализовать. Обновил мой ответ. И да, в формализме для mo_relaxed отсутствует эта гарантия, хотя у неатомарных объектов I думать она все еще есть. Это то, что комитет хотел бы исправить, но пока мы можем считать само собой разумеющимся, что это на самом деле запрещено. Это проблема только формальной проверки, а не реальной жизни.
"интересный мысленный эксперимент с безумными последствиями очень слабых правил упорядочения" Это то, что люди сказали о вещах, которые являются UB, но "работают на практике": Это безумие думать, что вы не получите 2compl на этих процессорах, поскольку единственный asm instr mult instr находится в 2compl... до тех пор, пока анализатор определяет, что x>0, так что xа>хb означает a>b, и ваш код, основанный на 2compl mult, не работает. Конечно, наивная компиляция МП ничего смешного не производит, но как быть с будущими агрессивными компиляторами? Мой код отсутствия гонки был очень простым, поэтому проблема должна быть четкой, но другие примеры менее ясны.
@curiousguy: Мой главный аргумент здесь не основан на компиляции для какой-либо конкретной ISA. Он основан на правилах самой абстрактной машины C, которые устанавливают границы того, что правило «как если бы» позволяет делать оптимизаторам. У меня есть раздел о вероятном аппаратном обеспечении, которое могло бы это сделать, которое не могло запустить C++, потому что оно мог изобретает значения из воздуха. Но суть этого ответа вообще не в этом.
Вы говорите, что Crazytown - это не реальный мир, исправление семантики OTOH привело бы к изменениям в испускаемом asm: "Это изменение будет означать более сильную семантику для memory_order_relaxed, что повлечет за собой дополнительные накладные расходы для memory_order_relaxed на ряде архитектур, включая ARM, текущие архитектуры Nvidia и, возможно, POWER."
@curiousguy: это изменение — это предложение в p0668r5: пересмотр модели памяти C++. Пункт маркера ясно показывает, что нет закрывает эту лазейку. Это несвязанное изменение. Они просто отмечают проблему «из воздуха», говоря об исправлении несвязанной проблемы. Я привел его как доказательство намерений комитета по стандартам. Так что нет, закрытие лазейки из воздуха не приведет к каким-либо изменениям в asm. Изменение, о котором они говорят, запретит переупорядочивание IRIW, чтобы все потоки могли согласовать глобальный порядок хранилищ, то есть модель с несколькими копиями атома.
@PeterCordes n3710 все путает: низкоуровневые/высокоуровневые определения «несет зависимость», которые связаны лишь смутно. Основная идея похоронена: станет ли SE неизбежным в какой-то момент. В какой-то момент авторы действительно казались потерянными («требуется перекомпиляция существующего кода»… привет? это действительно не самая серьезная проблема здесь). Суть вопроса: авторы считают, что вам нужно определить «цепочки зависимостей», чтобы исключить нежелательное поведение; это либо основано на синтаксисе (ужасно), либо вообще («мы рассматриваем каждое расслабленное хранилище, как если бы оно зависело от каждой предыдущей загрузки»)
Давайте продолжить обсуждение в чате.
(Stackoverflow жалуется на слишком много комментариев, которые я оставил выше, поэтому я собрал их в ответ с некоторыми изменениями.)
Перехват, который вы цитируете из стандартного рабочего проекта С++ N3337, был неверным.
[Note: The requirements do allow r1 == r2 == 42 in the following example, with x and y initially zero:
// Thread 1: r1 = x.load(memory_order_relaxed); if (r1 == 42) y.store(r1, memory_order_relaxed); // Thread 2: r2 = y.load(memory_order_relaxed); if (r2 == 42) x.store(42, memory_order_relaxed);
Язык программирования никогда не должен допускать этого «r1 == r2 == 42».
Это не имеет ничего общего с моделью памяти. Этого требует причинно-следственная связь, которая является основной логической методологией и основой любого дизайна языка программирования. Это фундаментальный контракт между человеком и компьютером. Любая модель памяти должна ей соответствовать. В противном случае это ошибка.
Причинность здесь отражается внутрипотоковыми зависимостями между операциями внутри потока, такими как зависимость данных (например, чтение после записи в одном и том же месте) и зависимость управления (например, операция в ветке) и т. д. Они не могут быть нарушены любая языковая спецификация. Любой проект компилятора/процессора должен учитывать зависимость в своем зафиксированном результате (т. е. видимом извне или программе).
Модель памяти в основном касается упорядочения операций с памятью среди нескольких процессоров, что никогда не должно нарушать внутрипотоковую зависимость, хотя слабая модель может допускать нарушение (или невидимость) причинно-следственной связи, происходящей в одном процессоре, в другом процессоре.
В вашем фрагменте кода оба потока имеют (внутрипотоковую) зависимость данных (загрузка->проверка) и зависимость управления (проверка->сохранение), которые обеспечивают упорядоченность их соответствующих исполнений (внутри потока). Это означает, что мы можем проверить вывод более поздней операции, чтобы определить, была ли выполнена более ранняя операция.
Затем мы можем использовать простую логику, чтобы сделать вывод, что если и r1, и r2 являются 42, должен существовать цикл зависимости, что невозможно, если только вы не удалите одну проверку условия, которая по существу прерывает цикл зависимости. Это не имеет никакого отношения к модели памяти, а зависит от данных внутри потока.
Причинность (или, точнее, внутрипоточная зависимость здесь) определена в C++ std, но не так явно в ранних версиях, потому что зависимость больше относится к терминологии микроархитектуры и компилятора. В спецификации языка это обычно определяется как операционная семантика. Например, управляющая зависимость, образованная «оператором if», определена в той же версии черновика, которую вы указали как «Если условие дает истину, выполняется первый подоператор». Это определяет последовательный порядок выполнения.
При этом компилятор и процессор могут запланировать выполнение одной или нескольких операций ветки if до того, как условие if будет разрешено. Но независимо от того, как компилятор и процессор планируют операции, результат if-ветви не может быть зафиксирован (т. е. стать видимым для программы) до того, как будет разрешено if-условие. Следует различать требования семантики и детали реализации. Один из них — спецификация языка, другой — то, как компилятор и процессор реализуют спецификацию языка.
На самом деле текущий проект стандарта C++ исправил эту ошибку в https://timsong-cpp.github.io/cppwp/atomics.order#9 с небольшим изменением.
[ Note: The recommendation similarly disallows r1 == r2 == 42 in the following example, with x and y again initially zero:
// Thread 1: r1 = x.load(memory_order_relaxed); if (r1 == 42) y.store(42, memory_order_relaxed); // Thread 2: r2 = y.load(memory_order_relaxed); if (r2 == 42) x.store(42, memory_order_relaxed);
Но для хранилища, да, необходимо, чтобы все предыдущие зависимости управления и данных были неспекулятивными, прежде чем сделать хранилище видимым для других потоков на любом нормальном оборудовании.
Обратите внимание, что формально стандарт по-прежнему говорит только «должен», а не «должен». запрещает, который вы выделили полужирным шрифтом, применяет только если, реализация следует за рекомендация в предыдущем пункте. Но да, это гораздо более убедительно сформулировано, чем предыдущее «следует запретить» внизу. Однако хорошая идея процитировать новую формулировку; Я сделал то же самое в своем ответе (с другим выбором жирного шрифта). Проголосовал за рассуждения о последовательном выполнении неатомарных вещей; Я не думаю, что все ваши рассуждения полностью подтверждаются, но в целом идея правильная.
@PeterCordes Да, две загрузки для if-condition и if-branch могут происходить не по порядку (либо по расписанию компилятора, либо по конвейеру процессора), но результат не может быть виден программе. То есть загруженное значение в if-ветке нельзя сохранить в переменную программы. Это (внутрипоточная) причинность, не связанная с другим потоком или ядром. Другому ядру не обязательно видеть эту причинно-следственную связь (за исключением модели согласованности памяти причинно-следственной связи). Они могут видеть не по порядку. Дело здесь в том, что семантика программы (внутри потока) всегда должна удовлетворять «причинности внутри потока».
При всех загрузках + еще не выполненной ветке в неупорядоченном бэкэнде возможен такой порядок: 2-я загрузка берет значение из кеша. 1-я загрузка берет значение из кеша. cmp+branch подтверждает предположение о переходе на основе значения 1-й загрузки. Затем позже второе значение загрузки может быть сохранено из регистра в память. Хранилище должно ждать, пока ветвь не станет спекулятивной, чтобы покинуть буфер хранилища, но критическое время для загрузки — это когда они считывают кэш L1d во время выполнения, а не когда они удаляются.
@PeterCordes Это зависит от того, как вы определяете «видимый». Если значение только сохраняется в переменную, но нигде не используется, т.е. приводит к какому-либо эффекту, это нормально. С внутренней точки зрения процессора это всегда возможно, а моя точка зрения - с точки зрения "внешней программы". Я предполагаю, что здесь наши определения не были полностью согласованы.
Добавление к моему предыдущему комментарию: или поток может сохранить оба результата загрузки в глобальные переменные, где другие потоки могут видеть, что он наблюдал загрузки не по порядку с некоторым известным порядком хранения. Это очень реальная вещь. Спекуляция ветвления нарушает зависимости данных. Порядок зависимостей asm, который mo_consume был разработан для моделирования, не работает с зависимостями управления, а только с зависимостями данных ALU. В любом случае, я не уверен, были ли вы не согласны с этим конкретным примером или мы просто не понимали друг друга.
@PeterCordes Да, для наблюдения за потоками можно увидеть неупорядоченные загрузки, если только это не модель согласованности причинно-следственных связей, которая, по-видимому, не соответствует тому, что определяет стандартный расслабленный порядок C++.
Есть причина, по которой по умолчанию стоит seq_cst: так что интуиция о причинно-следственной связи работает. Для LoadLoad это также будет работать, если первая загрузка является загрузкой-загрузкой.
Насколько мне известно, стандарт не дает вам такой защиты.