Может ли кто-нибудь дать простое объяснение того, как "Полные заборы" реализованы в.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 и другие архитектуры имеют свои собственные модели памяти - и, следовательно, гарантии упорядочения памяти (или их отсутствия) - и свои собственные инструкции / способы работы с памятью / упорядочением загрузки.

При параллельном программировании без блокировок следует заботиться о переупорядочении команд программы.

Изменение порядка команд программы может происходить в несколько этапов:

  1. Оптимизация компилятора C#/VB.NET/F#
  2. Оптимизация JIT-компилятора
  3. Оптимизация процессора.

Заборы памяти - единственный способ обеспечить определенный порядок инструкций вашей программы. По сути, ограничение памяти - это класс инструкций, который заставляет ЦП обеспечивать ограничение порядка. Заборы памяти можно разделить на три категории:

  1. Загрузите заборы - убедитесь, что инструкции по загрузке процессора не перемещаются через заборы
  2. Заборы магазина - убедитесь, что инструкции ЦП магазина не перемещаются через заборы
  3. Полные заборы - не загружайте или сохраняйте инструкции процессора, перемещающиеся через заборы

В.NET Framework существует множество способов создания заборов: Interlock, Monitor, ReaderWriterLockSlim и т. Д.

Thread.MemoryBarrier создает полную границу как на уровне JIT-компилятора, так и на уровне процессора.

Другие вопросы по тегам