Стандарты C ++ 11 / C ++ 14 в том виде, в котором они написаны, позволяют сворачивать / объединять три хранилища в одно хранилище окончательного значения. Даже в таком случае:
y.store(1, order);
y.store(2, order);
y.store(3, order); // inlining + constant-folding could produce this in real code
Стандарт не гарантирует, что наблюдатель, вращающийся на y
(с атомарной загрузкой или CAS), когда-либо увидит y == 2
. Программа, зависящая от этого, будет иметь ошибку гонки данных, но только гонку типа ошибки разнообразия сада, а не гонку данных типа неопределенного поведения C ++. (Это UB только с неатомарными переменными). Программа, которая ожидает иногда увидеть это, вовсе не обязательно содержит ошибки. (См. Ниже re: индикаторы выполнения.)
Любой порядок, который возможен на абстрактной машине C ++, может быть выбран (во время компиляции) в качестве порядка, который будет всегда. Это правило «как если бы» в действии. В этом случае это как если бы все три хранилища произошли друг за другом в глобальном порядке, без загрузки или сохранения из других потоков между y=1
и y=3
.
Это не зависит от целевой архитектуры или оборудования; точно так же, как переупорядочение во время компиляции расслабленных атомарных операций разрешено, даже если ориентированный на строго упорядоченный x86. Компилятор не должен сохранять ничего, что вы могли бы ожидать, думая об оборудовании, для которого вы компилируете, поэтому вам нужны барьеры. Барьеры могут компилироваться в нулевые инструкции asm.
Так почему компиляторы не делают эту оптимизацию?
Это проблема качества реализации, которая может изменить наблюдаемую производительность / поведение на реальном оборудовании.
Самый очевидный случай, когда это проблема, - это индикатор выполнения. Выведение хранилищ из цикла (который не содержит других атомарных операций) и сворачивание их всех в один приведет к тому, что индикатор выполнения останется на 0, а затем перейдет на 100% в самом конце.
В C ++ 11 std::atomic
не существует способа запретить это делать в тех случаях, когда вы этого не хотите, поэтому на данный момент компиляторы просто предпочитают никогда не объединять несколько атомарных операций в одну. (Объединение их всех в одну операцию не меняет их порядок относительно друг друга.)
Составители компиляторов правильно заметили, что программисты ожидают, что атомарное хранилище действительно будет происходить в памяти каждый раз, когда источник делает y.store()
. (См. Большинство других ответов на этот вопрос, в которых утверждается, что хранилища должны выполняться отдельно, поскольку возможные читатели ожидают увидеть промежуточное значение.) Т.е. это нарушает принцип наименьшего удивления.
Однако в некоторых случаях это было бы очень полезно, например, чтобы избежать бесполезного shared_ptr
ref count inc / dec в цикле.
Очевидно, что любое переупорядочение или объединение не может нарушать никаких других правил упорядочивания. Например, num++; num--;
по-прежнему должен быть полным барьером для переупорядочения во время выполнения и компиляции, даже если он больше не затрагивает память на num
.
Обсуждается возможность расширения std::atomic
API, чтобы дать программистам контроль над такой оптимизацией, после чего компиляторы смогут оптимизировать, когда это будет полезно, что может происходить даже в тщательно написанном коде, который не является преднамеренным. неэффективно. Некоторые примеры полезных кейсов для оптимизации упоминаются в следующих ссылках на обсуждения / предложения в рабочих группах:
См. Также обсуждение этой же темы в ответе Ричарда Ходжеса на Может ли num ++ быть атомарным для 'int num'? (см. Комментарии). См. Также последний раздел моего ответа на тот же вопрос, где я более подробно утверждаю, что такая оптимизация разрешена. (Оставляем это здесь коротким, потому что эти ссылки на рабочую группу C ++ уже подтверждают, что текущий стандарт в том виде, в котором он написан, допускает это, и что текущие компиляторы просто не оптимизируются специально.)
В рамках текущего стандарта volatile atomic<int> y
был бы одним из способов гарантировать, что его хранилища не могут быть оптимизированы. (Как указывает Херб Саттер в SO-ответе, volatile
и atomic
уже разделяют некоторые требования, но они разные). См. Также взаимосвязь std::memory_order
с volatile
на cppreference.
Доступ к volatile
объектам не может быть оптимизирован (например, потому что они могут быть отображенными в память регистрами ввода-вывода).
Использование volatile atomic<T>
в основном решает проблему с индикатором выполнения, но это некрасиво и может выглядеть глупо через несколько лет, если / когда C ++ выберет другой синтаксис для управления оптимизацией, чтобы компиляторы могли начать делать это на практике.
Я думаю, мы можем быть уверены, что компиляторы не начнут выполнять эту оптимизацию, пока не появится способ ее контролировать. Надеюсь, это будет своего рода подписка (например, memory_order_release_coalesce
), которая не изменит поведение существующего кода C ++ 11/14, когда он скомпилирован как C ++. Но это может быть похоже на предложение в wg21 / p0062: tag not-optimize case with [[brittle_atomic]]
.
wg21 / p0062 предупреждает, что даже volatile atomic
не решает всего, и не рекомендует использовать его для этой цели. Это дает такой пример:
if(x) {
foo();
y.store(0);
} else {
bar();
y.store(0); // release a lock before a long-running loop
for() {...} // loop contains no atomics or volatiles
}
// A compiler can merge the stores into a y.store(0) here.
Даже с volatile atomic<int> y
компилятору разрешено вытеснить y.store()
из if/else
и просто сделать это один раз, потому что он по-прежнему выполняет ровно 1 хранилище с тем же значением. (Что будет после длинного цикла в ветке else). Особенно если в магазине всего relaxed
или release
вместо seq_cst
.
volatile
действительно останавливает объединение, обсуждаемое в вопросе, но это указывает на то, что другие оптимизации на atomic<>
также могут быть проблематичными для реальной производительности.
Другие причины отказа от оптимизации включают: никто не написал сложный код, который позволил бы компилятору безопасно выполнять эту оптимизацию (ни разу не ошибаясь). Этого недостаточно, потому что N4455 утверждает, что LLVM уже реализует или может легко реализовать некоторые из упомянутых оптимизаций.
Причина, по которой программистов сбивает с толку, безусловно, правдоподобна. Код без блокировок достаточно сложно правильно написать.
Не будьте случайны в использовании атомного оружия: оно недешево и мало оптимизирует (в настоящее время совсем не оптимизирует). Однако не всегда легко избежать избыточных атомарных операций с std::shared_ptr<T>
, поскольку не существует его неатомарной версии (хотя один из ответов здесь дает простой способ определить shared_ptr_unsynchronized<T>
для gcc).
person
Peter Cordes
schedule
30.08.2017
f
- это только один поток из многих, пишущих вy
, в то время как другие читают изy
? Если компилятор объединяет записи в одну запись, поведение программы может неожиданно измениться. - person Some programmer dude   schedule 30.08.2017y
в42
между 2-м и 3-м хранилищами,y
все равно будет1
в концеf
. Если бы избыточные хранилища были удалены,y
будет2
в концеf
. - person Richard Critten   schedule 30.08.2017y
в42
между 2-м и 3-м хранилищами. Вы можете написать программу, которая просто делает магазин, и, возможно, вам повезет, но нет никакого способа гарантировать это. Невозможно сказать, произошло ли это из-за того, что избыточные записи были удалены, или из-за того, что вам просто не повезло с синхронизацией, следовательно, оптимизация действительна. Даже если это действительно произойдет, у вас нет возможности узнать, потому что это могло произойти до первого, второго или третьего. - person nwp   schedule 30.08.2017shared_ptr
экземпляры во встроенных функциях? - person curiousguy   schedule 14.12.2018