Могу ли я принуждать кеш к многоядерному процессору x86?

На другой неделе я написал небольшой класс потоков и односторонний канал сообщений, чтобы обеспечить связь между потоками (два канала на поток, очевидно, для двунаправленной связи). Все отлично работало на моем Athlon 64 X2, но мне было интересно, если бы у меня возникли проблемы, если бы оба потока смотрели на одну и ту же переменную, а локальное кешированное значение для этой переменной на каждом ядре не синхронизировалось.

Я знаю, что ключевое слово volatile заставит переменную обновляться из памяти, но есть ли способ для многоядерных процессоров x86 принудительно синхронизировать кеши всех ядер? Это что-то, о чем мне нужно беспокоиться, или будет изменчивым и правильное использование легких механизмов блокировки (я использовал _InterlockedExchange для установки переменных volatile pipe) обрабатывать все случаи, когда я хочу написать код "lock free" для многоядерных процессоров x86?

Я уже знаю и использовал критические разделы, мьютексы, события и т.д. Я в основном задаюсь вопросом, есть ли встроенные x86, что я не знаю, какой силой или может использоваться для обеспечения согласованности кеша.

Ответ 1

volatile только заставляет ваш код перечитывать значение, но не может контролировать, откуда оно читается. Если значение было недавно прочитано вашим кодом, то оно, вероятно, будет в кеше, и в этом случае volatile заставит его перечитать из кеша, а не из памяти.

В x86 не так много инструкций по связности кэша. Существуют инструкции prefetchnta, такие как prefetchnta, но это не влияет на семантику упорядочения памяти. Раньше это реализовывалось путем переноса значения в кэш L1 без загрязнения L2, но в современных разработках Intel с большим общим включающим кешем L3 все сложнее.

Процессоры x86 используют разновидность протокола MESI (MESIF для Intel, MOESI для AMD), чтобы их кэши были согласованы друг с другом (включая частные кэши L1 разных ядер). Ядро, которое хочет записать строку кэша, должно заставить другие ядра сделать недействительной свою копию, прежде чем оно сможет изменить свою собственную копию из общего в измененное состояние.


Вам не нужны никакие инструкции по забору (например, MFENCE) для создания данных в одном потоке и их использования в другом на x86, потому что загрузки/хранилища x86 имеют встроенную семантику получения/выпуска. Вам нужен MFENCE (полный барьер), чтобы получить последовательную последовательность. (Предыдущая версия этого ответа предполагала, что clflush, что неверно).

Вам нужно предотвратить переупорядочение во время компиляции, потому что модель памяти C++ слабо упорядочена. volatile старый, плохой способ сделать это; C++ 11 std :: atomic - намного лучший способ написания кода без блокировки.

Ответ 2

Когерентность кэша гарантируется между ядрами из-за протокола MESI, используемого процессорами x86. Вам нужно только беспокоиться о связности памяти при работе с внешним оборудованием, которое может получить доступ к памяти, в то время как данные по-прежнему находятся в кэшах ядра. Не похоже, что это ваш случай здесь, поскольку текст предполагает, что вы программируете в userland.

Ответ 3

Вам не нужно беспокоиться о когерентности кэша. Аппаратные средства позаботятся об этом. Возможно, вам придется беспокоиться о проблемах производительности из-за этой когерентности кэша.

Если ядро # 1 записывает данные в переменную, это делает недействительными все остальные копии строки кэша в других ядрах (потому что оно должно получить исключительное право собственности на строку кэша до фиксации хранилища). Когда ядро # 2 считывает ту же самую переменную, оно будет отсутствовать в кэше (если ядро # 1 не записало его обратно до уровня общего кэша).

Поскольку вся строка кэша (64 байта) должна считываться из памяти (или записываться обратно в общий кэш, а затем считываться ядром № 2), это будет иметь некоторые затраты производительности. В этом случае это неизбежно. Это желаемое поведение.


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

Этой стоимости можно избежать, убедившись, что эти переменные не находятся в одной строке кэша. Этот эффект известен как False Sharing, так как вы заставляете процессоры синхронизировать значения объектов, которые фактически не разделяются между потоками.

Ответ 4

Летучие не будут делать этого. В С++ волатильность влияет только на то, какие оптимизаторы компилятора, такие как хранение переменной в регистре, а не памяти, или удаление полностью.

Ответ 5

Вы не указали, какой компилятор вы используете, но если вы находитесь в окнах, посмотрите эту статью здесь. Также рассмотрите доступные здесь функции ynchronization s . Возможно, вы захотите отметить, что в целом volatile недостаточно, чтобы делать то, что вы хотите, но в VC 2005 и 2008 добавлены нестандартные семантики, которые добавляют скрытые ограничения памяти вокруг чтения и записи.

Если вы хотите, чтобы вещи были переносимыми, у вас будет намного сложнее дорога.

Ответ 6

Вот серия статей, объясняющих современные архитектуры памяти здесь, включая Кэши Intel Core2 и многие другие темы современной архитектуры.

Статьи очень читаемы и хорошо иллюстрированы. Наслаждайтесь!

Ответ 7

В вашем вопросе есть несколько вопросов, поэтому я отвечу им, насколько мне известно.

  • В настоящее время нет переносимого способа реализации блокировки в С++. Предложение С++ 0x решает это, введя библиотеку атомов.
  • Волатильность не гарантирует атомарность многоядерности, и ее реализация зависит от поставщика.
  • На x86 вам не нужно ничего делать, кроме объявлять общие переменные как изменчивые, чтобы предотвратить некоторые оптимизации компилятора, которые могут нарушить многопоточный код. Volatile сообщает компилятору не кэшировать значения.
  • Существуют некоторые алгоритмы (например, Dekker), которые не будут работать даже на x86 с изменчивыми переменными.
  • Если вы точно не знаете, что прохождение доступа к данным между потоками является основным узким местом в вашей программе, избегайте бесплатных решений. Используйте передаваемые данные по значению или блокировки.

Ответ 9

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

Изменить: если вы используете компилятор Intel или GCC, вы можете использовать атомные встроенные функции, которые, как представляется, делают все возможное для предохраняйте кеш, когда это возможно.