Статьи

Ложный обмен

Память хранится в системе кеша в единицах, известных как строки кеша. Строки кэша имеют степень 2 смежных байта, размер которых обычно составляет 32-256. Наиболее распространенный размер строки кэша составляет 64 байта. Ложный общий доступ — это термин, который применяется, когда потоки невольно влияют на производительность друг друга, изменяя независимые переменные, совместно использующие одну и ту же строку кэша. Конфликт записи в строках кэша является единственным наиболее ограничивающим фактором для достижения масштабируемости для параллельных потоков выполнения в системе SMP. Я слышал, что ложный обмен описан как тихий убийца производительности, потому что это далеко не очевидно при взгляде на код.

Чтобы добиться линейной масштабируемости с количеством потоков, мы должны обеспечить, чтобы два потока не записывали в одну переменную или строку кэша. Два потока, записывающих в одну и ту же переменную, могут отслеживаться на уровне кода. Чтобы узнать, разделяют ли независимые переменные одну и ту же строку кэша, нам нужно знать структуру памяти, или мы можем получить инструмент, который скажет нам. Intel VTune — такой инструмент профилирования. В этой статье я объясню, как распределяется память для объектов Java и как мы можем заполнить наши строки кэша, чтобы избежать ложного совместного использования.

Фигура 1.

Рисунок 1. выше иллюстрирует проблему ложного обмена. Поток, работающий на ядре 1, хочет обновить переменную X, тогда как поток на ядре 2 хочет обновить переменную Y. К сожалению, эти две горячие переменные находятся в одной строке кэша. Каждый поток будет бороться за право владения строкой кэша, чтобы они могли ее обновить. Если ядро ​​1 получает владение, подсистема кэша должна будет сделать недействительной соответствующую строку кэша для ядра 2. Когда Ядро 2 получит право собственности и выполнит свое обновление, ядру 1 будет приказано сделать недействительной свою копию строки кэша. Это будет пинг-понг туда и обратно через кэш-память третьего уровня, что сильно повлияет на производительность. Эта проблема будет еще более усугублена, если конкурирующие ядра находятся на разных сокетах и ​​дополнительно должны пересечь межсоединение сокетов.

Макет памяти Java

В JVM Hotspot все объекты имеют заголовок из 2 слов. Во-первых, это слово «метка», которое состоит из 24 битов для хэш-кода и 8 битов для флагов, таких как состояние блокировки, или его можно поменять местами для объектов блокировки. Второе — это ссылка на класс объекта. Массивы имеют дополнительное слово для размера массива. Каждый объект выровнен по 8-байтовой границе гранулярности для производительности. Поэтому, чтобы быть эффективными при упаковке, поля объекта переупорядочиваются из порядка объявления в следующий порядок на основе размера в байтах:

  1. двойной (8) и длинный (8)
  2. целые (4) и поплавки (4)
  3. шорты (2) и чулки (2)
  4. логические значения (1) и байты (1)
  5. ссылки (4/8)

С этим знанием мы можем заполнить строку кэша между любыми полями с 7 long. В
Disruptor мы добавляем строки кэша вокруг
курсора
RingBuffer и последовательностей BatchEventProcessor .

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

public final class FalseSharing
implements Runnable
{
public final static int NUM_THREADS = 4; // change
public final static long ITERATIONS = 500L * 1000L * 1000L;
private final int arrayIndex;

private static VolatileLong[] longs = new VolatileLong[NUM_THREADS];
static
{
for (int i = 0; i < longs.length; i++)
{
longs[i] = new VolatileLong();
}
}

public FalseSharing(final int arrayIndex)
{
this.arrayIndex = arrayIndex;
}

public static void main(final String[] args) throws Exception
{
final long start = System.nanoTime();
runTest();
System.out.println("duration = " + (System.nanoTime() - start));
}

private static void runTest() throws InterruptedException
{
Thread[] threads = new Thread[NUM_THREADS];

for (int i = 0; i < threads.length; i++)
{
threads[i] = new Thread(new FalseSharing(i));
}

for (Thread t : threads)
{
t.start();
}

for (Thread t : threads)
{
t.join();
}
}

public void run()
{
long i = ITERATIONS + 1;
while (0 != --i)
{
longs[arrayIndex].value = i;
}
}

public final static class VolatileLong
{
public volatile long value = 0L;
public long p1, p2, p3, p4, p5, p6; // comment out
}
}

Результаты

Запустив приведенный выше код, увеличив количество потоков и добавив / удалив заполнение строк кэша, я получаю результаты, показанные на рисунке 2. ниже. Это измерение продолжительности тестовых прогонов на моем 4-ядерном Nehalem дома.

Фигура 2.

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

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

Так что у вас есть это. Ложный обмен может быть тихим убийцей производительности.

 

От http://mechanical-sympathy.blogspot.com/2011/07/false-sharing.html