C # Shared Memory - риск кэширования ЦП (энергонезависимое чтение)?

Меня интересует реализация общей памяти в С#. MemoryMappedViewAccessor позволяет считывать данные из общей области памяти.

Теперь memoryMappedViewAccessor наследуется от UnmanagedMemoryAccessor, который предоставляет доступ к таким методам, как ReadInt32(), реализацию которого можно увидеть здесь https://referencesource.microsoft.com/#mscorlib/system/io/unmanagedmemoryaccessor.cs,7632fe79d4a8ae4c . В принципе, похоже, используется относительно простая небезопасная арифметика/приведение указателей,

 pointer += (_offset + position);
 result = *((Int32*)(pointer));

Но .. (как) гарантируется, что ЦП не будет кэшировать это значение? Например, вы можете пометить переменные как «volatile», чтобы обеспечить такое поведение, но как это сделать в случае, как указано выше, когда данные считываются из памяти по указателю через небезопасный код. Будет ли это всегда рассматриваться как изменчивое чтение или оно не является изменчивым?

В случае последнего, не означает ли это, что данные в общей памяти могут не синхронизироваться в реализации Microsoft, например. внешний процесс очень часто переопределяет определенное место в памяти, и код С# очень часто читает это, рискуя тем, что значения будут кэшироваться ЦП вместо того, чтобы каждый раз заново считываться из памяти?

Спасибо


person Bogey    schedule 11.04.2018    source источник
comment
Связанный: Может ли num++ быть атомарным для 'int num'? (C и x86 asm) объясняет когерентность кеша и что происходит, когда несколько процессоров читают / пишет одно и то же место.   -  person Peter Cordes    schedule 12.04.2018
comment
@PeterCordes Спасибо за ссылку, Питер. Я не уверен на 100% в своем собственном ответе и своих комментариях под ним, вы видите в них ошибки?   -  person Hadi Brais    schedule 12.04.2018
comment
@HadiBrais: я вообще не знаю C#, но когда я читал, это выглядело разумно. Но я думаю, что главное в том, что вы хотите, чтобы компилятор не кэшировал значение в register и вообще не выполнял другую загрузку. Неважно, могли ли вы с микроархитектурой объединить две реальные нагрузки, просто чтобы они обе происходили по отдельности. т. е. кеш является когерентным, поэтому кеширование процессора в первую очередь не представляет риска.   -  person Peter Cordes    schedule 12.04.2018


Ответы (1)


Эта часть кода *((Int32*)(pointer)) компилируется в машинную инструкцию, которая обращается к ячейке памяти. Таким образом, каждый раз, когда вызывается ReadInt32, будет осуществляться доступ к указанной ячейке памяти. Если «гарантировано ли, что ЦП не будет кэшировать это значение?» вы имеете в виду кеши ЦП, тогда когерентность кеша, реализованная аппаратно, обеспечит доступ к актуальному значению. В противном случае, если вы имеете в виду внутренние структуры буферизации, используемые для хранения загруженных данных до тех пор, пока инструкция, выпустившая загрузку, не будет удалена, то после того, как загрузка будет удалена, любые другие последующие загрузки в ту же ячейку памяти должны будут отправить другой запрос памяти в кеш. иерархия (внутренние буферы больше не содержат данных).

Тем не менее, две загрузки, которые достаточно близки в последовательности инструкций и обращаются к одной и той же ячейке памяти, могут быть объединены (при условии, что ЦП поддерживает такой метод), если между ними нет инструкций сериализации нагрузки (барьер нагрузки). Я не думаю, что это когда-либо произойдет с такой большой функцией, как ReadInt32 (принимая во внимание также весь код функций, вызываемых ReadInt32). Даже при последовательном вызове ReadInt32 без какого-либо кода между вызовами между двумя последовательными обращениями *((Int32*)(pointer)) будут сотни инструкций со всевозможными зависимостями, шанс совмещения двух чтений практически равен нулю на всех процессорах x86 и ARM. Процессор отменил бы первое чтение задолго до того, как увидит второе. Обратите внимание, что два последовательных чтения могут быть объединены в один запрос памяти.

person Hadi Brais    schedule 12.04.2018
comment
Большое спасибо за ваше подробное объяснение! Верна ли моя интерпретация вашего ответа, если, например, unsafe C# выполняет ((Int32)(pointer)) дважды подряд, например читает память (скажем, не с какими-либо вызовами функций между ними, а напрямую/впоследствии), и значение, хранящееся в этом адресе, изменяется извне только между этими двумя чтениями (например, общая память записывается другим процессом), C# гарантированно читать последнее значение при каждом вызове, ЕСЛИ гарантируется, что компилятор не выполняет никакой перекомпоновки/объединения этих операторов чтения? - person Bogey; 12.04.2018
comment
@Bogey Перестановка двух операций чтения в одно и то же место (компилятором или ЦП (законно для процессоров ARM, но не x86)) без промежуточных инструкций может повлиять на правильность программы. Это связано с тем, что второе чтение в программном порядке может получить более старое значение, чем первое чтение в программном порядке, если они были переупорядочены. Объединение операций чтения допустимо как для процессоров ARM, так и для процессоров x86, и может привести к задержке получения обновленного значения. Таким образом, гарантируется только то, что в конце концов вы получите самое последнее значение, но не гарантируется, какой доступ для чтения сделает это... - person Hadi Brais; 12.04.2018
comment
@Bogey ... и когда это произойдет. Это можно контролировать только с помощью барьеров чтения. В противном случае чтение считается одновременным с любым другим доступом от других ядер. Однако когерентность кеша гарантирует, что любой доступ к любому из кешей с любого из ядер будет когерентным (будут видны одинаковые и самые последние значения). - person Hadi Brais; 12.04.2018