Где находится поколение 0?

#.net #multithreading #memory-management #garbage-collection

#.net #многопоточность #управление памятью #сбор мусора

Вопрос:

У меня возникло небольшое недопонимание по поводу .Кэш сети и процессора. Я думал, что в кэше процессора был сохранен только стек потоков, но, по-видимому, часть кучи, конкретно Gen 0, фактически выделена в кэше CPU L2.

Я прочитал несколько вещей вроде: Начальный предел размера поколения 0 определяется размером кэша процессора.

Но что произойдет, когда поколение 0 больше, чем размер кэша процессора? Затем разделяется между оперативной памятью и кэшем? как? в противном случае полностью перемещается в оперативную память? Я прочитал комментарии людей, утверждающих, что у них был Gen 0 размером около 500 Мб, поэтому маловероятно, что у них был кэш процессора 500 МБ.

Насколько я знаю (и я могу ошибаться), объекты в Gen 0 могут совместно использоваться потоками, так как же можно совместно использовать объект в потоках, запланированных на разных процессорах, если он хранится в кэше процессора? Делает .СЕТЬ заботится о том, чтобы поместить объект в ОЗУ, если он не локальный?

Комментарии:

1. Я бы порекомендовал вам прочитать известное disruptor.googlecode.com/files/Disruptor-1.0.pdf или либо механическое сочувствие. blogspot.cz/2011/07 /… . Там также есть очень красивые картинки и понятные описания. Несмотря на то, что оно написано «для Java», кэширование на уровне процессора не зависит от языка 🙂

Ответ №1:

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

Все ваши вопросы требуют довольно общих ответов. Простой ответ заключается в том, что многопоточное программирование очень сложно, и если вы так не думаете, у вас действительно еще не так много опыта 🙂 Как только вы поймете, какое огромное количество допущений и оптимизаций производительности выполняют процессоры, вы также поймете, что C на самом деле не намного ближе к «реальному оборудованию», чем C #.

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

Теперь, конечно, если вы выполняете какую-то изолированную работу, связанную с процессором, вы можете извлечь большую выгоду из возможности поместить всю память, с которой вы работаете, в кэш процессора. Вы можете помочь этому, только используя структуры данных достаточно маленького размера — вы не можете принудительно кэшировать бит информации или что-либо еще (фактически, все, что вы читаете из памяти, будет в тот или иной момент находиться в кэше процессора — процессор не может читать напрямую из ОЗУ — ОЗУ слишком медленное). Если вы можете поместить все свои данные в кеш, и никто не заставляет вас удаляться из кеша (помните, многозадачная среда), вы можете получить потрясающую производительность даже от традиционно дорогостоящих операций (например, много скачков в памяти, а не последовательный доступ и т.д.).

Однако, как только вам нужно обмениваться данными между потоками, у вас начинаются проблемы. Вам нужна синхронизация, чтобы убедиться, что два процессора (или ядра процессора, я не собираюсь их различать) действительно работают с одними и теми же данными!

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

 while (!aborted)
{
  ...
}
  

Теоретически, вполне возможно, что это просто окажется бесконечным циклом. Агрессивный компилятор может увидеть, что вы никогда не меняете значение aborted и просто замените !aborted на true (.NET не будет), или он может сохранить значение aborted внутри регистра.

Регистры вообще не синхронизируются автоматически по умолчанию. Это может быть серьезной проблемой, если тело цикла потока достаточно простое. По мере того, как вы все глубже погружаетесь в многопоточное программирование, вы будете полностью опустошены кодом, который вы использовали для написания, и предположениями, которые у вас были.

Самое важное, что нужно помнить, это то, что все эти оптимизации, выполняемые компиляторами и процессорами, гарантированно не изменят поведение, только если вы запускаете их изолированно и в одном потоке. Когда вы нарушаете это предположение, начинается настоящий ад.

Комментарии:

1. Среда выполнения .NET может принудительно кэшировать данные, используя инструкции предварительной выборки. Он просто не может гарантировать, что он останется там.

2. Также за кэш команд может отвечать среда выполнения, когда она выполняет codegen, поскольку ответственность за удаление сгенерированного кода из кэша данных лежит на JITs.

3. @user1937198 Трудно быть уверенным в этих вещах, поскольку это часть (довольно закрытой) реализации самой среды выполнения. И, учитывая, насколько вероятно, что такого рода операции (загрузка всей кучи поколения 0 в кэш) будут очень дорогими и принесут очень мало пользы, я почти уверен . NET этого не делает. А что касается JIT, я не совсем понимаю, как это поможет повысить производительность. ЧИСТАЯ сила, что — строки кэша будут удалены довольно быстро, я думаю. Но это всего лишь (несколько обоснованные) догадки.

4. Первым был счетчик, который вы абсолютно не можете этого сделать. Второй комментарий — проблема с корректностью. Процессоры x86 не гарантируют, что если вы запишете что-то в виде данных, а затем начнете выполнять это, вы выполните данные, которые вы записали, если вы явно не очистите кэши I $ и D $. И это именно то, что JIT делает во время codegen.

5. @user1937198 — Действительно. Intel автоматически обрабатывает множество сложных вещей, которые чипы RISC оставляют составителю компилятора (или даже пользователю, например, примерно в 1998 году или около того, я портировал приложение Win32 с x86 на DEC Alpha и получил несколько проблем с выравниванием памяти).).