вопрос
(1) Что такое строка кэша ЦП?
(2) Что такое барьер памяти?
(3) Что такое псевдосовместное использование?
(4) Как избежать ложного обмена?
Архитектура кэш-памяти процессора
ЦП является сердцем компьютера, и все операции и программы в конечном итоге выполняются им.
Основная память (ОЗУ) — это место, где хранятся данные, и между ЦП и основной памятью есть несколько уровней кэша, потому что даже прямой доступ к основной памяти очень медленный.
Если вы выполняете одну и ту же операцию с частью данных несколько раз, имеет смысл загружать ее рядом с ЦП, когда операция выполняется, например, подсчет циклов, вы не хотите запускать в основную память каждый раз, когда выполняете цикл чтобы получить эти данные, чтобы вырастить их.

Кэши ближе к ЦП быстрее и меньше.
Таким образом, кэш L1 небольшой, но быстрый и близок к ядру ЦП, которое его использует.
L2 больше и медленнее, и по-прежнему может использоваться только одним ядром ЦП.
L3 чаще встречается в современных многоядерных машинах, еще больше, медленнее и используется всеми ядрами ЦП на одном сокете.
Наконец, основная память содержит все данные для запуска программы, она больше, медленнее и используется всеми ядрами ЦП на всех сокетах.
Когда ЦП выполняет операцию, он сначала переходит в L1 для поиска необходимых данных, затем в L2, затем в L3, и, наконец, если ни один из этих кешей не существует, необходимые данные отправляются в основную память для их получения.
Чем дальше вы идете, тем больше времени занимает вычисление.
Поэтому, если вы выполняете какие-то очень частые операции, убедитесь, что данные находятся в кеше L1.
Строка кэша процессора
Кэш состоит из строк кэша, обычно 64 байта (64 байта для обычных процессоров, 32 байта для старых процессоров), и фактически относится к блоку адресов в основной памяти.
Длина Java составляет 8 байт, поэтому в одной строке кэша можно хранить 8 переменных long.

Во время работы программы кэш загружается из основной памяти последовательно по 64 байта для каждого обновления. Итак, если вы обращаетесь к массиву типа long, когда одно значение в массиве загружается в кеш, остальные 7 элементов также будут загружены в кеш.
Однако если вы используете структуру данных, элементы которой не расположены в памяти рядом друг с другом, например связанный список, вы не получите преимущества бесплатной загрузки кэша.
Однако у этой бесплатной загрузки есть и обратная сторона. Представьте, что если у нас есть переменная a типа long, которая не является частью массива, а является отдельной переменной, и рядом с ней есть еще одна переменная b типа long, то при загрузке a b будет загружена бесплатно.
Кажется, в этом нет ничего плохого, но если поток на одном ядре ЦП модифицирует a , поток на другом ядре ЦП читает b .
Когда прежний изменяет a, a и b будут загружены в строку кеша бывшего ядра одновременно.После обновления a все остальные строки кеша, содержащие a, будут недействительными, потому что a в других кешах не является последним значением. .
Когда последний считывает b, он обнаруживает, что строка кэша стала недействительной и ее необходимо перезагрузить из основной памяти.
Помните, что наши кеши обрабатываются в единицах строк кеша, поэтому аннулирование кеша a также сделает недействительным и b, и наоборот.

Так что есть проблема, b и a совершенно не важны, но каждый раз, когда обновление a нужно перечитать из основной памяти, оно тормозится промахами кеша.
Это легендарный псевдо-обмен.
ложный обмен
Что ж, после того, как мы представили архитектуру кэша процессора и механизм строки кэша выше, давайте перейдем к нашей теме — ложному совместному использованию.
Когда несколько потоков изменяют переменные, которые независимы друг от друга, если эти переменные совместно используют одну и ту же строку кэша, это непреднамеренно повлияет на производительность друг друга, что является ложным разделением.
Давайте взглянем на следующий пример, который полностью иллюстрирует, что такое ложное совместное использование.
public class FalseSharingTest {
public static void main(String[] args) throws InterruptedException {
testPointer(new Pointer());
}
private static void testPointer(Pointer pointer) throws InterruptedException {
long start = System.currentTimeMillis();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100000000; i++) {
pointer.x++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100000000; i++) {
pointer.y++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(System.currentTimeMillis() - start);
System.out.println(pointer);
}
}
class Pointer {
volatile long x;
volatile long y;
}
В этом примере мы объявляем класс Pointer, который содержит две переменные x и y (должны быть объявлены как volatile для обеспечения видимости, мы поговорим о барьерах памяти позже), поток увеличивает x в 100 миллионов раз, поток увеличивает y 100 миллионов раз.
Видно, что х вообще не имеет ничего общего с у, но при обновлении х другие строки кэша, содержащие х, будут инвалидированы, и в то же время будет инвалидирован также у. Время вывода работы этой программы равно3890ms
.
Избегайте ложного обмена
Мы знаем принцип ложного разделения.Кэш-строка составляет 64 байта, а длинный тип — 8 байт, поэтому избежать ложного разделения очень просто.Автор суммирует следующие три способа:
(1) Добавьте 7 длинных типов между двумя переменными длинных типов.
Мы изменили указанный выше указатель на следующую структуру:
class Pointer {
volatile long x;
long p1, p2, p3, p4, p5, p6, p7;
volatile long y;
}
Запустите программу еще раз, и вы обнаружите, что время вывода волшебным образом сократилось до695ms
.
(2) Воссоздайте свой собственный длинный тип вместо длинного, который поставляется с java
Измените указатель следующим образом:
class Pointer {
MyLong x = new MyLong();
MyLong y = new MyLong();
}
class MyLong {
volatile long value;
long p1, p2, p3, p4, p5, p6, p7;
}
заодно поставилpointer.x++;
превратиться вpointer.x.value++;
,Пучокpointer.y++;
превратиться вpointer.y.value++;
, снова запустите программу и обнаружите, что время724ms
.
(3) Используйте аннотацию @sun.misc.Contended (java8)
Измените MyLong следующим образом:
@sun.misc.Contended
class MyLong {
volatile long value;
}
Использование этой аннотации по умолчанию недопустимо и должно быть добавлено в параметры запуска JVM.-XX:-RestrictContended
вступит в силу, снова запустите программу и обнаружите, что время718ms
.
Обратите внимание, что первые два из трех вышеперечисленных методов реализуются путем добавления полей, а добавленные поля не имеют места для использования и могут быть оптимизированы JVM, поэтому рекомендуется использовать третий метод.
Суммировать
(1) ЦП имеет многоуровневый кэш, чем ближе кэш к ЦП, тем меньше и быстрее;
(2) Данные в кеше ЦП обрабатываются в единицах поведения кеша;
(3) Линия кэша ЦП может принести пользу бесплатной загрузки данных, поэтому производительность обработки массивов очень высока;
(4) Линия кэша ЦП также имеет недостатки: при многопоточной обработке нерелевантных переменных они будут влиять друг на друга, то есть ложное совместное использование;
(5) Основная идея предотвращения ложного совместного использования состоит в том, чтобы не допустить появления нерелевантных переменных в одной и той же строке кэша;
(6) Один состоит в том, чтобы добавить семь длинных типов между каждыми двумя переменными;
(7) Второй — создать свой собственный длинный тип вместо использования родного;
(8) В-третьих, использовать аннотации, предоставляемые java8;
пасхальные яйца
Какие классы в java избегают вмешательства ложного обмена?
Помните синтаксический анализ исходного кода ConcurrentHashMap, который мы представили ранее?
Метод size() внутри построен с использованием идеи сегментации Класс, используемый для каждого сегмента, — CounterCell, и его класс имеет аннотацию @sun.misc.Contended.
Если вы не знаете, вы можете подписаться на мою общедоступную учетную запись «Брат Тонг, читающий исходный код», чтобы проверить исторические новости и найти эту статью.
В дополнение к этому классу в java есть еще один LongAdder, который также использует эту аннотацию, чтобы избежать ложного совместного использования.В следующей главе мы вместе изучим анализ исходного кода LongAdder, так что следите за обновлениями.
Знаете ли вы какие-либо другие приложения, которые избегают ложного обмена?
Добро пожаловать, чтобы обратить внимание на мою общедоступную учетную запись «Брат Тонг читает исходный код», проверить больше статей из серии исходного кода и поплавать в океане исходного кода с братом Тонгом.
