Почему назначение целых чисел на естественно выровненной переменной атома на x86?

Я читал эту статью об атомных операциях и упоминает, что 32-битное целочисленное назначение является атомарным на x86, если переменная естественно выровнена.

Почему естественное выравнивание обеспечивает атомарность?

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

Процессоры часто делают такие вещи, как кэш-доступ или пересылку кеш-строк между ядрами, в кусках размером от 2 до 2, поэтому границы выравнивания, меньшие, чем линия кэша, имеют значение. (См. Комментарии @ BeeOnRope ниже). См. Также Atomicity на x86 для более подробной информации о том, как CPU реализует атомные нагрузки или хранит внутренне, и может ли num ++ быть атомарным для ‘int num’? для получения дополнительной информации о том, как атомарные операции RMW, такие как atomic::fetch_add() / lock xadd , реализуются внутри.


Во-первых, это предполагает, что int обновляется одной инструкцией хранилища, а не записывает разные байты отдельно. Это часть того, что std::atomic гарантии, но этот простой C или C ++ этого не делает. Однако это будет нормально . Система x86-64 V ABI не запрещает компиляторам делать доступ к int переменным неатомным, хотя для этого требуется, чтобы int был 4B с выравниванием по умолчанию 4B. Например, x = a<<16 | b x = a<<16 | b может скомпилироваться в два отдельных 16-разрядных хранилища, если захочет компилятор.

Гонки данных - это неопределенное поведение как на C, так и на C ++, поэтому компиляторы могут и предполагают, что память не изменяется асинхронно. Для кода, который гарантированно не сломается, используйте C11 stdatomic или C ++ 11 std :: atomic . В противном случае компилятор будет просто сохранять значение в регистре, а не перезагружать каждый раз, когда вы его читаете , например, volatile но с фактическими гарантиями и официальной поддержкой из языкового стандарта.

До C ++ 11 атомные операторы обычно делались с volatile или другими вещами, а здоровая доза «работает над компиляторами, о которых мы заботимся», поэтому C ++ 11 стал огромным шагом вперед. Теперь вам больше не нужно заботиться о том, что делает компилятор для простого int ; просто используйте atomic . Если вы обнаружите, что старые гиды говорят об атомарности int , они, вероятно, предшествуют C ++ 11.

 std::atomic shared; // shared variable (compiler ensures alignment) 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 or XCHG is much slower than a simple store 

Замечание: для atomic больше, чем CPU может делать атомарно (так что .is_lock_free() является ложным), см. Где lock для std :: atomic? , int и int64_t / uint64_t блокируются во всех основных компиляторах x86.


Таким образом, нам просто нужно поговорить о поведении insn как mov [shared], eax .


TL; DR: ISA x86 гарантирует, что естественно-ориентированные магазины и нагрузки являются атомными, до 64 бит. Поэтому компиляторы могут использовать обычные магазины / нагрузки, если они гарантируют естественное выравнивание std::atomic .

(Но обратите внимание, что i386 gcc -m32 не может сделать это для 64-битных типов C11 _Atomic , только привязывая их к 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 году изменением на заголовок. Однако это не изменило поведение C11.)


IIRC, были системы SMP 386, но текущая семантика памяти не была установлена ​​до 486. Вот почему в руководстве написано «486 и новее».

Из «Руководства разработчика программного обеспечения Intel® 64 и IA-32», том 3 », выделенные курсивом . (см. также wiki для x86 для ссылок: текущие версии всех томов или прямую ссылку на страницу 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-битных и более узких нагрузках / магазинах, являющихся атомарными

Таким образом, integer, x87 и MMX / SSE загружают / сохраняют до 64b, даже в 32-битном или 16-битном режиме (например, movq , movsd , movhps , pinsrq , extractps и т. Д.) Являются атомарными, если данные выровнены. gcc -m32 использует movq xmm, [mem] для реализации атомных 64-разрядных нагрузок для таких вещей, как std::atomic . Clang4.0 -m32 к сожалению, использует ошибку lock cmpxchg8b 33109 .

На некоторых процессорах с внутренними каналами данных 128b или 256b (между исполнительными единицами и L1 и между различными кэшами), векторные нагрузки / хранилища 128b и даже 256b являются атомарными, но это не гарантируется никаким стандартом или легко запрашивается во время выполнения, к сожалению, для компиляторов, реализующих std::atomic<__int128> или 16B structs .

Если вы хотите атомный 128b для всех систем x86, вы должны использовать lock cmpxchg16b (доступную только в режиме 64 бит). (И он не был доступен в процессорах первого поколения x86-64. Вам нужно использовать -mcx16 с gcc / clang, чтобы они могли его испускать .)

Даже процессоры, которые внутренне выполняют атомарные нагрузки / хранилища 128b, могут проявлять неатомное поведение в многопроцессорных системах с протоколом согласованности, который работает в небольших кусках: например, AMD Opteron 2435 (K10) с streamами, работающими на отдельных сокетах, связанных с 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 для совместного использования грязных линий кеширования непосредственно между кешами в разных ядрах, поэтому одно kernel ​​может считывать из его допустимой копии строки кэша, пока обновления к нему поступают из другого кеша.

    Intel использует MESIF , который требует, чтобы грязные данные распространялись на большой общий общий кеш L3, который выступает в качестве блокиратора для когерентного трафика. L3 содержит tags с кэшем 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 с cs:eip call или far- jmp . (И дальний вызов подталкивает вещи в стек.) IDK, если это считается единственным 48-битным доступом или отдельными 16 и 32-битными.

Были предприняты попытки формализовать модель памяти x86, последняя из которых является документом x86-TSO (расширенная версия) с 2009 года (ссылка из раздела упорядочения памяти вики-файла x86 ). Это не полезно, так как они определяют некоторые символы, чтобы выразить вещи в их собственных обозначениях, и я не пытался их прочитать. IDK, если он описывает правила атомарности, или если он касается только упорядочения памяти.


Atomic Read-Modify-Write

Я упомянул cmpxchg8b , но я говорил только о загрузке, и каждый из них отдельно был атомарным (т. cmpxchg8b », когда одна половина нагрузки поступает из одного магазина, другая половина - из другого магазина).

Чтобы содержимое содержимого этой памяти не cmpxchg8b между загрузкой и хранилищем, вам необходимо lock cmpxchg8b , так же, как вам нужно lock inc [mem] чтобы весь read-modify-write был атомарным. Также обратите внимание, что даже если cmpxchg8b без lock делает одну атомную нагрузку (и, возможно, хранилище), вообще небезопасно использовать ее как нагрузку 64b с ожидаемым = желательным. Если значение в памяти соответствует ожидаемому, вы получите неатомное чтение-изменение-запись этого местоположения.

Префикс lock делает даже несвязанные обращения, которые пересекают границы кеш-строки или страницы атома, но вы не можете использовать его с mov чтобы сделать негласное хранилище или загрузить атом. Он может использоваться только с инструкциями чтения-изменения-записи в память-назначения, такими как add [mem], eax .

( lock xchg reg, [mem] в xchg reg, [mem] , поэтому не используйте xchg с mem для сохранения размера кода или подсчета команд, если производительность не имеет значения. Используйте его только тогда, когда вы хотите использовать барьер памяти и / или атомный обмен, или когда размер кода - это единственное, что имеет значение, например, в загрузочном секторе.)

См. Также: Может ли num ++ быть атомарным для 'int num'?


Зачем lock mov [mem], reg не существует для атомных неустановленных хранилищ

Из справочника insn ref (руководство Intel x86 vol2), cmpxchg :

Эта инструкция может использоваться с префиксом LOCK чтобы позволить команде выполняться атомарно. Чтобы упростить интерфейс к шине процессора, операнд назначения получает цикл записи без учета результата сравнения. Операнд назначения записывается обратно, если сравнение не выполняется; в противном случае исходный операнд записывается в пункт назначения. ( Процессор никогда не производит блокировку чтения без создания заблокированной записи .)

Это конструктивное решение уменьшило сложность набора микросхем до того, как controller памяти был встроен в CPU. Он все еще может сделать это для lock инструкций в регионах MMIO, которые попадают на шину PCI-Express, а не DRAM. Было бы просто ввести в заблуждение для lock mov reg, [MMIO_PORT] для создания записи, а также для чтения в регистр ввода-вывода с отображением памяти.

Другое объяснение состоит в том, что не очень сложно убедиться, что ваши данные имеют естественное выравнивание, а lock store будет работать ужасно по сравнению с простое выравнивание ваших данных. Было бы глупо тратить транзисторы на то, что было бы настолько медленным, что его не стоило бы использовать. Если вам это действительно нужно (и не прочь прочитать память тоже), вы можете использовать xchg [mem], reg (XCHG имеет неявный префикс LOCK), который еще медленнее, чем гипотетический lock mov .

Использование префикса lock также является полным барьером памяти, поэтому он налагает на себя служебную нагрузку за пределы только атомного RMW. т.е. x86 не может выполнять расслабленный атомный RMW (без промывки буфера хранилища). Другие ISA могут, поэтому использование .fetch_add(1, memory_order_relaxed) может быть быстрее на не-x86.

mfence факт: до mfence существовало существование, обычной идиомой была lock add dword [esp], 0 , которая является не-оператором, кроме флагов clobbering и выполняет заблокированную операцию. [esp] почти всегда горячий в кеш-памяти L1 и не вызывает конкуренции с каким-либо другим kernelм. Эта идиома может быть более эффективной, чем MFENCE, как изолированный барьер памяти, особенно на процессорах AMD.

xchg [mem], reg - это, пожалуй, самый эффективный способ реализации хранилища последовательной согласованности, против mov + mfence , как для Intel, так и для AMD. mfence на Skylake по крайней мере блокирует выполнение команд без памяти не по xchg , но xchg и другие lock не работают. Компиляторы, отличные от gcc, используют xchg для магазинов, даже если они не заботятся о чтении старого значения.


Мотивация для этого дизайнерского решения:

Без него программное обеспечение должно было бы использовать 1-байтовые блокировки (или какой-либо ansible атомный тип) для защиты доступа к 32-битным целым, что крайне неэффективно по сравнению с общим доступом к атомному чтению для чего-то вроде глобальной переменной временной метки, обновляемой прерыванием таймера , Вероятно, в кремнии, по-видимому, бесплатно, чтобы гарантировать выравнивание доступа ширины шины или меньше.

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


Поскольку вы предложили щедрость, я предполагаю, что вы искали длинный ответ, который блуждал по всем интересным темам. Сообщите мне, есть ли вещи, которые я не рассматривал, которые, по вашему мнению, сделают этот Q & A более ценным для будущих читателей.

Поскольку вы связали одного в вопросе , я настоятельно рекомендую прочитать больше сообщений в блоге Jeff Preshing . Они превосходны и помогли мне собрать fragmentы того, что я знал, в понимание упорядоченности памяти в C / C ++ source vs. asm для разных аппаратных архитектур и как / когда сообщать компилятору, что вы хотите, t непосредственно писать asm.

Если 32-разрядный или меньший объект естественным образом выровнен в «нормальной» части памяти, любой 80386 или совместимый процессор, отличный от 80386sx, сможет читать или записывать все 32 бита объекта за одну операцию. Хотя способность платформы делать что-то быстрым и полезным способом не обязательно означает, что платформа по какой-то причине иногда не делает ее каким-то другим способом, и хотя я считаю, что на многих, а не на всех процессорах x86, возможно, имеют области памяти, к которым можно получить доступ только 8 или 16 бит за раз, я не думаю, что Intel когда-либо определяла какие-либо условия, когда запрос на выравнивание 32-разрядного доступа к «нормальной» области памяти приведет к чтению системы или написать часть стоимости, не читая и не записывая все это, и я не думаю, что Intel намерена когда-либо определять какую-либо такую ​​вещь для «нормальных» областей памяти.

Естественно выровненный означает, что адрес типа кратен размеру этого типа.

Например, байт может быть по любому адресу, короткий (предположительно 16 бит) должен быть кратным 2, int (предположительно 32 бита) должен быть кратным 4, а длинный (предположительно 64 бита) должен быть кратным 8.

В случае, если вы получаете доступ к части данных, которые не выровнены естественным образом, процессор либо поднимет ошибку, либо будет читать / записывать память, но не как атомную операцию. Действие CPU будет зависеть от архитектуры.

Например, изображение у нас есть макет памяти ниже:

 01234567 ...XXXX. 

а также

 int *data = (int*)3; 

Когда мы пытаемся прочитать *data байты, которые составляют значение, распределяются между 2 блоками размера int, 1 байт находится в блоке 0-3 и 3 байта находятся в блоке 4-7. Теперь, только потому, что блоки логически рядом друг с другом, это не значит, что они физически. Например, блок 0-3 может быть в конце строки кэша процессора, в то время как блок 3-7 находится в файле страницы. Когда процессор переходит к блоку 3-7 доступа, чтобы получить 3 байта, в которых он нуждается, он может видеть, что блок не находится в памяти и сигнализирует, что ему нужна память. Это, вероятно, блокирует вызывающий процесс, в то время как ОС страниц назад.

После того, как память была загружена, но до того, как ваш процесс разбудит резервную копию, еще один может прийти и написать Y к адресу 4. Затем ваш процесс перенесен, и процессор завершит чтение, но теперь он прочитал XYXX, а не XXXX вы ожидали.

Если бы вы спрашивали, почему он сконструирован так, я бы сказал, что это хороший побочный продукт от дизайна архитектуры процессора.

Вернемся к 486 раз, нет многоядерного процессора или QPI-ссылки, поэтому атомарность на данный момент не является строгим требованием (может потребоваться DMA?).

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

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

Если мы учтем только то, что в статье, которую вы связываете, – инструкции присваивания , то выравнивание гарантирует атомарность, потому что MOV (инструкция назначения) является атомарным по дизайну на выровненных данных.

Другими видами инструкций, например INC, должны быть LOCK ed (префикс x86, который предоставляет эксклюзивный доступ к общей памяти текущему процессору в течение всей операции с префиксом), даже если данные выравниваются, потому что они фактически выполняются через несколько шаги (= инструкции, а именно load, inc, store).

  • Пункты памяти и стиль кодирования по Java VM
  • Возможна ли память в ConcurrentBag?
  • Как заставить BackgroundWorker возвращать объект
  • Безопасно ли читать указатель на функцию одновременно без блокировки?
  • Выбор лучшего списка параллелизма в Java
  • ОЖИДАНИЕ на sun.misc.Unsafe.park (родной метод)
  • Инструкции SSE: какие процессоры могут выполнять атомные операции памяти 16B?
  • В чем разница между Thread start () и Runnable run ()
  • .NET Асинхронный stream чтения / записи
  • Есть ли способ для нескольких процессов совместно использовать прослушивающий сокет?
  • Более эффективный способ для цикла паузы
  • Interesting Posts

    Поддерживает ли Windows 10 звук 5.1 через USB

    хранить и извлекать объект classа по общим предпочтениям

    кеширование результатов с помощью услуги angular2 http

    Как получить заголовок с карточек или похожих предметов с одинаковой высотой с помощью гибкой коробки?

    Лётная загрузка против желаемой загрузки

    Как скопировать столбец с условным форматом «значение» – Libreoffice 4.1

    Визуализация файлов и каталогов

    Центр обновления Windows уничтожает мое интернет-соединение

    PriorityQueue.toString неверный порядок элементов

    Зависимые и фиксирующие взаимозависимости

    Google Maps v3 частично загружается в верхнем левом углу, событие изменения размера не работает

    показать форму windows из windows службы

    Можно ли заполнить массив номерами строк, которые соответствуют определенным критериям без цикла?

    Каков ваш метод маркировки «folksonomy» для файлов на вашей локальной машине?

    Являются ли IEnumerable методы Linq streamобезопасными?

    Давайте будем гением компьютера.