Извините за большое количество писем. (
Почти все инструкции в x86 ISA (кроме так называемых строковых инструкций и, возможно, нескольких других), включая CMPXCHG, являются атомарными в контексте одноядерного ЦП. Это связано с тем, что в соответствии с архитектурой x86 ЦП проверяет поступившие прерывания после каждого выполнения инструкции, а не в середине. В результате запрос прерывания может быть обнаружен и его обработка может быть запущена только на границе между выполнением двух последовательных инструкций. В связи с этим все ссылки на память, используемые ЦП во время выполнения одной инструкции, изолированы и не могут быть чередованы никакими другими действиями. Такое поведение характерно для одноядерных и многоядерных процессоров. Но если в контексте одноядерного ЦП имеется только один блок системы, который осуществляет доступ к памяти, в контексте многоядерного ЦП имеется более одного блока системы, который одновременно осуществляет доступ к памяти. Изоляция инструкций недостаточна для согласованности в такой среде, поскольку доступ к памяти, осуществляемый разными процессорами в одно и то же время, может чередовать друг друга. Благодаря этому дополнительный уровень защиты должен применяться к протоколу изменения данных. Для x86 этот уровень является префиксом блокировки, который инициирует атомарную транзакцию на системной шине.
Резюме. Безопасно и дешевле использовать команды синхронизации, такие как CMPXCHG, XADD, BTS и т. Д., Без префикса блокировки, если вы уверены, что к данным, доступным по этой инструкции, может обращаться только одно ядро. Если вы не уверены в этом, примените префикс блокировки для обеспечения безопасности за счет снижения производительности.
Существует два основных подхода к поддержке аппаратной синхронизации ЦП:
- На основе атомарной транзакции.
- На основе протокола согласованности кэша.
Никто не является серебряной пулей. Оба подхода имеют свои преимущества и недостатки.
Подход, основанный на атомарных транзакциях, основан на поддержке специального типа транзакций на шине памяти. Во время такой транзакции только один агент (ядро ЦП), подключенное к шине, имеет право доступа к памяти. В результате, с одной стороны, все ссылки на память, сделанные владельцем шины во время атомарной транзакции, гарантированно будут выполнены как одна непрерывная транзакция. С другой стороны, все остальные агенты шины (ядра ЦП) будут вынуждены ждать завершения атомарной транзакции, чтобы получить возможность доступа к памяти. Неважно, к каким ячейкам памяти они хотят получить доступ, даже если они хотят получить доступ к области памяти, на которую не обращается владелец шины во время атомарной транзакции. В результате широкое использование инструкций с префиксом блокировки значительно замедлит работу системы. С другой стороны, из-за того, что арбитр шины предоставляет доступ к шине для каждого агента шины в соответствии с циклическим планированием, существует гарантия, что каждый агент шины будет иметь относительно справедливый доступ к памяти, и все агенты будут удалось добиться прогресса и сделал это с той же скоростью. Кроме того, проблема ABA вступает в игру в случае атомарных транзакций, потому что по своей природе атомарные транзакции очень короткие (несколько ссылок на память, сделанные одной инструкцией), и все действия, выполняемые с памятью во время транзакции, зависят только от значения области памяти. без учета того, что область памяти была доступна кому-то еще между двумя транзакциями.
Хорошим примером поддержки синхронизации на основе атомарных транзакций является архитектура x86, в которой инструкции с префиксом блокировки заставляют ЦПУ выполнять их в атомарных транзакциях.
Подход, основанный на протоколе когерентности кэша, основан на том факте, что строка памяти может кэшироваться только в одном кэше L1 в один момент времени. Протокол доступа к памяти в системе когерентности кэша аналогичен следующей последовательности действий:
- CPU A хранит строку памяти X в кеше L1. В то же время CPU B желает получить доступ к строке памяти X. (X -> CPU A L1)
- ЦП B выдает строку памяти X транзакции доступа на шине. (X -> CPU A L1)
- Все агенты шины (ядра ЦП) имеют так называемый агент отслеживания, который прослушивает все транзакции на шине и проверяет, хранится ли строка памяти, к которой запрашивалась транзакция, в кэш-памяти L1 ЦП своего владельца. Таким образом, CPU A snooping agent обнаруживает, что CPU A владеет строкой памяти, запрошенной CPU B. (X -> CPU A L1)
- CPU A транзакция приостановки доступа к памяти, выданная CPU B. (X -> CPU A L1)
- CPU A очищает строку памяти, запрошенную B, из своего кэша L1. (X -> память)
- CPU Возобновление ранее приостановленной транзакции. (X -> память)
- CPU B извлекает строку памяти X из памяти. (X -> CPU B L1)
Благодаря этому протоколу ядро ЦП всегда получает доступ к актуальным данным в памяти, а доступ к памяти сериализуется в строгом порядке, один доступ во времени.
Поддержка синхронизации на основе протокола когерентности кэша основана на том факте, что ЦП может легко обнаружить, что к определенной линии памяти обращались между двумя временными точками. Во время первого доступа к строке X, которая должна открыть транзакцию, CPU может пометить, что строка памяти в кэше L1 должна контролироваться агентом отслеживания. В свою очередь, отслеживающий агент может во время очистки строки кэша дополнительно выполнить проверку, чтобы определить, отмечена ли строка для контроля, и поднять внутренний флаг, если контролируемая строка очищена. В результате, если ЦП будет проверять внутренний флаг во время доступа к памяти, который закрывает транзакцию, он будет знать, что контролируемая строка памяти может быть изменена кем-то другим, и приходит к выводу, что транзакция должна быть выполнена успешно или должна рассматриваться как неудачная.
Это способ реализации класса команд LL \ SC. Этот подход более простой, чем атомарные транзакции, и обеспечивает гораздо большую гибкость в синхронизации, поскольку на его основе может быть построено гораздо большее количество различных синхронизирующих примитивов по сравнению с подходом атомарных транзакций. Этот подход является более масштабируемым и эффективным, потому что он не блокирует доступ к памяти для всех других частей системы. И, как вы можете видеть, это решает проблему ABA, потому что основано на факте обнаружения доступа к области памяти, а не на значении обнаружения изменения области памяти. Любой доступ к области памяти, участвующей в текущей транзакции, будет рассматриваться как сбой транзакции. И это может быть хорошим и плохим одновременно, потому что конкретный алгоритм может интересоваться только значением области памяти и не принимает во внимание тот факт, что кто-то посередине получил доступ к местоположению, пока этот доступ не изменит память , В этом случае чтение значения памяти в середине приведет к ложному отрицательному сбою транзакции. Кроме того, такой подход может привести к значительному снижению производительности потоков управления, содержащихся в одной и той же строке памяти, поскольку они способны постоянно соединять линии памяти друг от друга и тем самым препятствовать успешному завершению транзакции друг друга. Это действительно серьезная проблема, потому что в терминальном случае это может превратить систему в живую блокировку.
Поддержка синхронизации на основе протокола кэширования обычно используется в процессоре RISC из-за простоты и гибкости. Но следует отметить, что Intel решила поддержать такой подход для поддержки синхронизации и в архитектуре x86. В прошлом году Intel объявила о расширении Transactional Synchronization для архитектуры x86, которое будет реализовано в процессорах Intel Haswell поколения. В результате, похоже, x86 получит самую мощную поддержку синхронизации и позволит разработчикам систем использовать преимущества обоих подходов.