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

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

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

Ответы

Ответ 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 ", выделенные курсивом. (см. также теги 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 года ( ссылка из раздела упорядочения памяти tag 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.

Ответ 2

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

Ответ 3

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

Например, байт может быть по любому адресу, короткий (предположительно 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. Затем ваш процесс будет перенесен, а CPU завершит чтение, но теперь он имеет прочитайте XYXX, а не ожидаемый XXXX.

Ответ 4

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

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

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

Ответ 5

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

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

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