Ответ 1
"Естественное" выравнивание означает выравнивание по его собственной ширине. Таким образом, загрузка/хранилище никогда не будет разбита на любую границу, более широкую, чем сама (например, страница, кеш-строка или еще более узкий размер блока, используемый для передачи данных между различными кешами).
Во-первых, это предполагает, что int
обновляется одной инструкцией хранилища, а не записывает разные байты отдельно. Это часть того, что гарантирует std::atomic
, но это простой C или С++. Однако это будет нормально. x86-64 System V ABI не запрещает компиляторам делать доступ к int
переменным неатомным, даже если для этого требуется int
будет 4B с выравниванием по умолчанию 4B.
Горы данных Undefined Поведение как на C, так и на С++, поэтому компиляторы могут и предполагают, что память не изменяется асинхронно. Для кода, который гарантированно не сломается, используйте C11 stdatomic или С++ 11 std:: atomic. В противном случае компилятор просто сохранит значение в регистре, а не перезагружается каждый раз, когда вы его прочитаете.
std::atomic<int> shared; // shared variable (in aligned memory)
int x; // local variable (compiler can keep it in a register)
x = shared.load(std::memory_order_relaxed);
shared.store(x, std::memory_order_relaxed);
// shared = x; // don't do that unless you actually need seq_cst, because MFENCE is much slower than a simple store
Таким образом, нам просто нужно поговорить о поведении insn как mov [shared], eax
.
TL; DR: ISA x86 гарантирует, что естественно упорядоченные хранилища и нагрузки являются атомными, до 64 бит. Таким образом, компиляторы могут использовать обычные магазины/нагрузки, если они гарантируют, что std::atomic<T>
имеет естественное выравнивание.
(Но обратите внимание, что i386 gcc -m32
не может сделать это для C11 _Atomic
64-битных типов, только выравнивая их с 4B, поэтому atomic_llong
на самом деле не атомарно. https://gcc.gnu.org/bugzilla/show_bug.cgi?id=65146#c4). g++ -m32
с std::atomic
отлично, по крайней мере, в g++ 5, потому что https://gcc.gnu.org/bugzilla/show_bug.cgi?id=65147 был установлен в 2015 году путем изменения <atomic>
. Однако это не изменило поведение C11.)
IIRC, были системы SMP 386, но текущая семантика памяти не была установлена до 486. Вот почему руководство говорит "486 и новее".
Из "Руководства разработчика программного обеспечения Intel® 64 и IA-32", том 3 ", выделенные курсивом. (см. также x86 теги wiki для ссылок: текущие версии всех томов или прямая ссылка на страница 256 из vol3 pdf от декабря 2015 г.)
В терминологии x86 слово "слово" - это два 8-битных байта. 32 бита представляют собой двойное слово или DWORD.
Раздел 8.1.1 Гарантированные атомные операции
Процессор Intel486 (и более новые процессоры с тех пор) гарантирует, что следующая базовая память операции всегда выполняются атомарно:
- Чтение или запись байта
- Чтение или запись слова, выровненного на 16-битной границе
- Чтение или запись двойного слова, выровненного на 32-битной границе (Это еще один способ сказать "естественное выравнивание" )
Эта последняя точка, выделенная жирным шрифтом, является ответом на ваш вопрос: это поведение является частью того, что требуется для процессора как процессора x86 (т.е. реализации ISA).
Остальная часть раздела обеспечивает дополнительные гарантии для более новых процессоров Intel: Pentium расширяет эту гарантию до 64 бит.
Процессор Pentium (и более новые процессоры с тех пор) гарантирует, что последующие операции с памятью всегда будут выполняться атомарно:
- Чтение или запись квадлового слова, выровненного на 64-битной границе (например, x87 load/store
double
илиcmpxchg8b
(что было новым в Pentium P5))- 16-разрядный доступ к нераскрытым ячейкам памяти, которые подходят к 32-разрядной шине данных.
В разделе далее указывается, что доступ к разнесению по линиям кэша (и границам страниц) не гарантированно является атомарным и:
"Инструкция x87 или инструкции SSE, которые обращаются к данным, большим, чем квадловое слово, могут быть реализованы с использованием множественный доступ к памяти."
Руководство AMD соглашается с Intel относительно согласованных 64-битных и более узких нагрузок/хранилищ, являющихся атомарными.
Таким образом, 64-разрядные x87 и MMX/SSE загружают/сохраняют до 64b (например, movq
, movsd
, movhps
, pinsrq
, extractps
и т.д.) являются атомарными, если данные выровнены. gcc -m32
использует movq xmm, [mem]
для реализации атомных 64-разрядных нагрузок для таких вещей, как std::atomic<int64_t>
. Clang4.0 -m32
к сожалению использует lock cmpxchg8b
ошибка 33109.
На некоторых процессорах с внутренними каналами данных 128b или 256b (между исполнительными модулями и L1 и между различными кэшами), 128b и даже 256b векторных нагрузок/хранилищ являются атомарными, но это не гарантируется никаким стандартным или легко запрашиваемым при запуске -time, к сожалению для компиляторов, реализующих структуры std::atomic<__int128>
или 16B.
Если вы хотите использовать атомный 128b для всех систем x86, вы должны использовать lock cmpxchg16b
(доступно только в режиме 64 бит). (И он не был доступен в процессорах первого поколения x86-64. Вам нужно использовать -mcx16
с gcc/clang чтобы они могли его исправить.)
Даже процессоры, которые внутренне выполняют атомарные нагрузки/хранилища 128b, могут проявлять неатомное поведение в многопроцессорных системах с протоколом согласованности, который работает в небольших кусках: например, AMD Opteron 2435 (K10) с потоками, запущенными на отдельных сокетах, связанных с HyperTransport.
Руководства для Intel и AMD расходятся для несвязанного доступа к кэшируемой памяти. Общим подмножеством для всех процессоров x86 является правило AMD. Cacheable означает регионы обратной записи или записи в памяти, а не несовместимые или комбинированные записи, как установлено в областях PAT или MTRR. Они не означают, что кэш-строка уже должна быть горячей в кэше L1.
- Intel P6 и более поздние версии гарантируют атомарность для кэшируемых нагрузок/хранилищ до 64 бит до тех пор, пока они находятся в одной кеш-линии (64B или 32B на очень старых процессорах, таких как PentiumIII).
-
AMD гарантирует атомарность для кэшируемых нагрузок/хранилищ, которые вписываются в единый 8B-выровненный кусок. Это имеет смысл, потому что мы знаем из теста 16B-store на многопроцессорном Opteron, что HyperTransport передает только в 8B кусках и не блокируется при передаче, чтобы предотвратить разрывы. (См. Выше). Я думаю,
lock cmpxchg16b
должен быть обработан специально.Возможно, связано: AMD использует MOESI для совместного использования грязных строк кеша непосредственно между кешами в разных ядрах, поэтому одно ядро может читать от его действительной копии строки кэша, пока обновления к ней поступают из другого кеша.
Intel использует MESIF, для чего требуется, чтобы грязные данные распространялись на большой общий общий кеш L3, который действует как блокиратор для обеспечения согласованности трафик. L3 содержит теги с кэшем L2/L1 для каждого ядра, даже для строк, которые должны находиться в состоянии Invalid в L3 из-за того, что M или E в кэше L1 для ядра. Путь данных между L3 и кэшами для каждого ядра составляет всего 32B в Haswell/Skylake, поэтому он должен буферизировать или что-то, чтобы избежать записи в L3 из одного ядра, происходящего между чтениями двух половин строки кэша, что может привести к разрыву граница 32B.
Соответствующие разделы руководств:
Процессоры семейства P6 (и более новые процессоры Intelпоскольку) гарантируют, что следующая дополнительная операция памяти будет всегда выполняться атомарно:
- Unaligned 16-, 32- и 64-битный доступ к кэшированной памяти, которая вписывается в строку кэша.
Руководство AMD64 7.3.2 Атомарность доступа
Кэшируемые, естественно выровненные одиночные нагрузки или хранилища до квадратного слова атомарны на любом процессоре модели, а также несогласованные нагрузки или хранилища менее квадратного слова, которые содержатся полностью в естественно выровненном квадрате
Обратите внимание, что AMD гарантирует атомарность для любой нагрузки, меньшей, чем qword, но Intel только для мощностей размером 2. 32-битный защищенный режим и 64-битный длинный режим могут загружать 48 бит m16:32
в качестве операнда памяти в cs:eip
с far- call
или far- jmp
. (И многозадачность выталкивает материал в стек.) IDK, если это считается единственным 48-битным доступом или отдельными 16 и 32-битными.
Были попытки формализовать модель памяти x86, последняя из которых - документ x86-TSO (расширенная версия) с 2009 года ( ссылка из раздела упорядочения памяти x86tag wiki). Это не полезно скрыть, поскольку они определяют некоторые символы, чтобы выразить вещи в их собственных обозначениях, и я не пытался их прочитать. IDK, если он описывает правила атомарности, или если он касается только упорядочения памяти.
Atomic Read-Modify-Write
Я упомянул cmpxchg8b
, но я говорил только о загрузке, и каждый из них отдельно был атомарным (т.е. не было "разрывов", когда одна половина нагрузки из одного хранилища, другая половина нагрузки - от другой магазин).
Чтобы содержимое содержимого этой памяти не изменялось между загрузкой и хранилищем, вам нужно lock
cmpxchg8b
, как вам нужно lock inc [mem]
для всего режима чтения-изменения -видите, чтобы быть атомным. Также обратите внимание, что даже если cmpxchg8b
без lock
выполняет одну атомную нагрузку (и, необязательно, хранилище), вообще небезопасно использовать ее в качестве нагрузки 64b с ожидаемым = желательным. Если значение в памяти соответствует ожидаемому, вы получите неатомное чтение-изменение-запись этого местоположения.
Префикс lock
делает даже несвязанные обращения, которые пересекают границы строки кеширования или страницы, атомы, но вы не можете использовать его с mov
, чтобы сделать негласное хранилище или загрузить атом. Он может использоваться только с инструкциями чтения-изменения-назначения памяти-назначения, такими как add [mem], eax
.
(lock
неявна в xchg reg, [mem]
, поэтому не используйте xchg
с mem, чтобы сохранить размер кода или количество команд, если производительность не имеет значения. Используйте его только тогда, когда вы хотите, чтобы барьер памяти и/или атомный обмен или когда размер кода - единственное, что имеет значение, например, в загрузочном секторе.)
См. также: Может ли num ++ быть атомарным для 'int num'?
Почему lock mov [mem], reg
не существует для атомных неустановленных хранилищ
Из справочника insn ref (руководство Intel x86 vol2), cmpxchg
:
Эта инструкция может использоваться с префиксом
lock
, чтобы разрешить инструкция должна выполняться атомарно. Чтобы упростить интерфейс для шина процессоров, операнд назначения получает цикл записи независимо от результата сравнения. Назначение операнд записывается обратно, если сравнение не выполняется; в противном случае источник операнд записывается в пункт назначения. ( Процессор никогда не производит заблокированное чтение без создания записи с фиксированной записью.)
Это конструктивное решение уменьшило сложность набора микросхем до того, как контроллер памяти был встроен в CPU. Он может все еще сделать это для инструкций lock
ed в областях MMIO, которые попадают на шину PCI-Express, а не DRAM. Было бы просто запутать для lock mov reg, [MMIO_PORT]
создание записи, а также чтение в регистр ввода-вывода с отображением памяти.
Другим объяснением является то, что не очень сложно убедиться, что ваши данные имеют естественное выравнивание, а lock store
будет работать ужасно по сравнению с простое выравнивание ваших данных. Было бы глупо тратить транзисторы на то, что было бы настолько медленным, что его не стоило бы использовать. Если вам это действительно нужно (и не прочь читать память тоже), вы можете использовать xchg [mem], reg
(XCHG имеет неявный префикс LOCK), который еще медленнее, чем гипотетический lock mov
.
Использование префикса lock
также является полным барьером памяти, поэтому он накладывает на служебную работу производительность только за пределы атомного RMW. (Удовлетворительный факт: до mfence
существовала общая идиома lock add [esp], 0
, которая является не-оператором, кроме флагов clobbering и выполняет заблокированную операцию. [esp]
почти всегда горяча в кэше L1 и не вызывает конкуренции с любым другим ядром. Эта идиома может по-прежнему быть более эффективной, чем MFENCE на процессорах AMD.)
Мотивация для этого проектного решения:
Без него программное обеспечение должно было бы использовать 1-байтовые блокировки (или какой-либо доступный атомный тип) для защиты доступа к 32-битным целым, что крайне неэффективно по сравнению с общим доступом к ядерному чтению для чего-то вроде глобальной переменной временной метки, обновленной прерывание таймера. Он, по-видимому, в принципе свободен в кремнии, чтобы гарантировать согласованные обращения ширины шины или меньше.
Для того чтобы блокировка была вообще возможной, требуется какой-то атомный доступ. (На самом деле, я думаю, аппаратное обеспечение могло бы обеспечить какой-то совершенно другой механизм блокировки с помощью аппаратного обеспечения.) Для CPU, который выполняет 32-битные передачи на своей внешней шине данных, имеет смысл иметь это единицу атомарности.
Поскольку вы предложили щедрость, я предполагаю, что вы искали длинный ответ, который блуждал по всем интересным темам. Дайте мне знать, есть ли вещи, которые я не рассматривал, которые, по вашему мнению, сделают этот Q & более ценным для будущих читателей.
Поскольку вы связали один в вопросе, , я настоятельно рекомендую прочитать больше сообщений в блоге Jeff Preshing. Они превосходны и помогли мне собрать фрагменты того, что я знал, в понимание упорядоченности памяти в C/С++ source vs. asm для разных аппаратных архитектур и как/когда сообщать компилятору, что вы хотите, t непосредственно писать asm.