Может ли кто-нибудь дать простое объяснение того, как "Полные заборы" реализованы в.Net с использованием Threading.MemoryBarrier?
Я ясно об использовании MemoryBarrier, но не о том, что происходит за кулисами во время выполнения. Кто-нибудь может дать хорошее объяснение того, что происходит?
2 ответа
В действительно сильной модели памяти испускание инструкций забора было бы ненужным. Все обращения к памяти будут выполняться по порядку, и все хранилища будут видны глобально.
Ограждения памяти необходимы, потому что современные распространенные архитектуры не обеспечивают надежную модель памяти - x86/x64 может, например, изменить порядок чтения относительно записи. (Более подробный источник - "Руководство разработчика программного обеспечения для архитектуры Intel® 64 и IA-32, 8.2.2 Упорядочение памяти в P6 и более поздних семействах процессоров"). Как пример из gazillions, алгоритм Деккера потерпит неудачу на x86 / x64 без заборов.
Даже если JIT создает машинный код, в котором аккуратно размещаются инструкции с загрузками и хранилищами памяти, его усилия бесполезны, если ЦП затем переупорядочивает эти нагрузки и сохраняет - что может, пока сохраняется иллюзия последовательной согласованности для текущего контекст / нить.
Риск чрезмерного упрощения: это может помочь визуализировать нагрузки и запасы, возникающие в результате потока инструкций, как стадо громовых диких животных. Когда они пересекают узкий мост (ваш процессор), вы никогда не можете быть уверены в порядке животных, так как некоторые из них будут медленнее, некоторые быстрее, некоторые обгоняют, некоторые отстают. Если в начале, когда вы генерируете машинный код, вы разделяете их на группы, помещая между ними бесконечно длинные ограждения, вы можете, по крайней мере, быть уверенными, что группа A предшествует группе B.
Заборы обеспечивают порядок чтения и записи. Формулировка не точная, но:
- Ограждение магазина "ждет" завершения всех ожидающих операций сохранения (записи), но не влияет на загрузку.
- Ограничение нагрузки "ждет" завершения всех ожидающих операций загрузки (чтения), но не влияет на хранилища.
- полный забор "ждет" всех операций по складированию и загрузке до конца. Это имеет эффект, который читает и пишет до того, как забор будет выполнен до того, как записи и загрузки будут на "другой стороне забора" (приходят позже, чем забор).
То, что JIT испускает для полного забора, зависит от архитектуры (CPU) и того, какой порядок памяти гарантирует это. Поскольку JIT точно знает, на какой архитектуре он работает, он может выдавать правильные инструкции.
На моей машине x64, с.NET 4.0 RC, это lock or
,
int a = 0;
00000000 sub rsp,28h
Thread.MemoryBarrier();
00000004 lock or dword ptr [rsp],0
Console.WriteLine(a);
00000009 mov ecx,1
0000000e call FFFFFFFFEFB45AB0
00000013 nop
00000014 add rsp,28h
00000018 ret
Руководство разработчика программного обеспечения для архитектуры Intel® 64 и IA-32, глава 8.1.2:
"... заблокированные операции сериализуют все незавершенные операции загрузки и сохранения (то есть ждут их завершения)"...."Заблокированные операции являются атомарными по отношению ко всем другим операциям с памятью и всем видимым извне событиям. Только выборка инструкций и доступ к таблице страниц могут передавать заблокированные инструкции. Заблокированные инструкции могут использоваться для синхронизации данных, записанных одним процессором и считанных другим процессором. ".
инструкции поупорядочению памяти решают эту конкретную задачу.
MFENCE
можно было бы использовать в качестве полного барьера в вышеупомянутом случае (по крайней мере, теоретически - для одной блокированные операции могли бы быть быстрее, для двух это могло бы привести к другому поведению).MFENCE
и его друзей можно найти в главе 8.2.5 "Усиление или ослабление модели упорядочения памяти".
Есть еще несколько способов сериализации магазинов и загрузок, хотя они либо нецелесообразны, либо медленнее, чем описанные выше методы:
В главе 8.3 вы можете найти полные инструкции по сериализации, такие как
CPUID
, Они также сериализуют поток команд: "Ничто не может передать команду сериализации, и команда сериализации не может передать любую другую инструкцию (чтение, запись, выборка команд или ввод / вывод)".Если вы установите память как сильную некэшированную (UC), это даст вам сильную модель памяти: не будет разрешен спекулятивный или неупорядоченный доступ, и все обращения будут появляться на шине, поэтому нет необходимости выдавать инструкцию.:) Конечно, это будет немного медленнее, чем обычно.
...
Так что это зависит от. Если бы существовал компьютер с надежными гарантиями заказа, JIT, вероятно, ничего бы не испустил.
IA64 и другие архитектуры имеют свои собственные модели памяти - и, следовательно, гарантии упорядочения памяти (или их отсутствия) - и свои собственные инструкции / способы работы с памятью / упорядочением загрузки.
При параллельном программировании без блокировок следует заботиться о переупорядочении команд программы.
Изменение порядка команд программы может происходить в несколько этапов:
- Оптимизация компилятора C#/VB.NET/F#
- Оптимизация JIT-компилятора
- Оптимизация процессора.
Заборы памяти - единственный способ обеспечить определенный порядок инструкций вашей программы. По сути, ограничение памяти - это класс инструкций, который заставляет ЦП обеспечивать ограничение порядка. Заборы памяти можно разделить на три категории:
- Загрузите заборы - убедитесь, что инструкции по загрузке процессора не перемещаются через заборы
- Заборы магазина - убедитесь, что инструкции ЦП магазина не перемещаются через заборы
- Полные заборы - не загружайте или сохраняйте инструкции процессора, перемещающиеся через заборы
В.NET Framework существует множество способов создания заборов: Interlock, Monitor, ReaderWriterLockSlim и т. Д.
Thread.MemoryBarrier создает полную границу как на уровне JIT-компилятора, так и на уровне процессора.