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

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

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

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

9 ответов


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

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

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


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

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


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


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

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

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


Volatile этого не сделает. В C++ volatile влияет только на оптимизацию компилятора, такую как хранение переменной в регистре вместо памяти или ее полное удаление.


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

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


есть ряд статей, объясняющих современные архитектуры памяти здесь, включая Интел Сердечником2 схроны и многие другие темы современной архитектуры.

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


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

  1. В настоящее время нет портативного способа реализации взаимодействия без блокировки на C++. Предложение C++0x решает эту проблему, представляя библиотеку atomics.
  2. Volatile не гарантирует атомарность на многоядерном, и его реализация зависит от поставщика.
  3. на x86 вам не нужно делать ничего особенного, кроме объявления shared переменные как volatile, чтобы предотвратить некоторые оптимизации компилятора, которые могут нарушить многопоточный код. Volatile говорит компилятору не кэшировать значения.
  4. есть некоторые алгоритмы (например, Dekker), которые не будут работать даже на x86 с изменчивыми переменными.
  5. Если вы не знаете наверняка, что передача доступа к данным между потоками является основным узким местом производительности в вашей программе, держитесь подальше от решений без блокировки. Использовать передачу данных по значению или замки.

следующая хорошая статья в отношении использования volatile W / threaded программы.

Volatile почти бесполезно для многопоточного программирования.


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

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