Это – перевод статьи “Threading in Java. Part II. Hardware” автора Beka Kodirov. Первая часть статьи находится здесь.
Расскажите об иерархии кэшей L1/L2/L3? Зачем они нужны?
Кэши L1/L2/L3 это сегменты памяти, в чем-то схожие с RAM. Это быстрая память небольшого объёма, которая используется для уменьшения времени доступа процессора к данным и снижения накладных расходов, связанных с простоями. Кэши разных уровней физически расположены в разных местах. Так кэш L1 расположен на ядре процессора, L2 – между процессором и оперативной памятью, L3 опционален и может быть встроен в материнскую плату. Архитектура кэшей разных уровней значительно отличается. Так, например, L1 использует транзисторы большего размера, разменивая мощность и объем памяти на скорость. Кэши более высоких уровней упакованы плотнее и используют транзисторы поменьше. Когда процессору требуется прочитать или записать данные в оперативную память, сперва проверяется их наличие в кэше. Если они есть, то используется кэшированное значение. Это намного быстрее, чем чтение или запись из основной памяти.
- L1 – самый ближний к процессору (каждое ядро имеет свой кэш). Обычно имеется один кэш для данных и один для машинных инструкций. Размеры варьируются в пределах 8-64 кб.
- L2 может делиться между несколькими ядрами и обычно колеблется в пределах 2-4 мб.
- L3 может быть так же расположен на кристалле процессора и достигать 8-16 мб. В некоторых процессорах он заменён кэшем L2.
Что такое “Cache line”?
Передача данных между основной памятью и кэшем выполняется блоками фиксированного размера, которые называются cache lines. Когда cache line копируется из памяти в кэш, в последнем создаётся cache entry. Кэш разделён на линии по 4-64 байта, иногда называемые блоками. При передаче данных между процессором и основной памятью cache line читается или записывается целиком. У каждого cache line есть метка, которая содержит адрес в основной памяти, показывающий откуда был скопирован блок . В современных CPU размер cache line обычно равен 64 байта.
Что такое “False sharing”? Плохо ли это? Как с этим быть?
Давайте рассмотрим пример:
public final class X { public volatile int f1; public volatile int f2; }
Все поля класса помечены как volatile
(см. статью блога. – прим. переводчика), это означает, что они могут быть использованы несколькими потоками и нужно гарантировать видимость записи в переменную для всех. В результате JVM возьмёт эту работу на себя, но что будет происходить “под капотом”?
Core0
знает о том, что cache line ‘n’ принадлежит не только ему, поэтому после каждого изменения он через шину уведомляет другие ядра о том, что их кэш устарел (при помощи сообщения Invalidate). Core1
слушает шину и инвалидирует соответствующий cache line. Хотя ядра работают с разными полями, такое поведение провоцирует лишний траффик на шине. Этот феномен известен как false sharing. Он влияет только на производительность и не нарушает целостность данных. Как с этим быть? Читайте ниже!
Что такое Memory padding?
В предыдущем примере мы рассматривали объект размером 32 байта, в то время как размер cache line равен 64 байтам. Класс X
содержит два поля f1
и f2
и оба лежат в одном cache line. Из-за частой инвалидации, чтение и запись переменных становятся более затратными. Чтобы решить эту проблему можно развести f1
и f2
по разным cache line‘ам, тогда изменение f1
не будет влиять на операции с f2
. Можно попробовать поместить несколько полей между f1
и f2
так, чтобы они в итоге оказались в разных cache line‘ах. Это может не сработать в зависимости от используемой JVM, так как виртуальная машина самостоятельно оптимизирует укладку полей в памяти.
В Java 8 была введена аннотация @Contended (см. JEP-142). С ней поля могут объявляться в порядке их укладки. Текущая реализация OpenJDK надлежащим образом сместит поле, добавив 128 байт после каждого аннотированного члена класса. Значение в 128 байт не случайно, это в два раза больше типичного размера cache line. Оно было выбрано с учетом cache prefetching algorithms – алгоритмов, которые агрегируют соседние cache line‘ы.
Что такое параллелизм потоков? Параллелизм задач?
Пример: мы запускаем код на двухпроцессорной системе (ЦПУ-1 “а” и ЦПУ-2 “б”) в параллельном окружении и хотим выполнить некоторые задачи “А” и “Б”. Можно выполнять “А” на процессоре “а” и “Б” на процессоре “б” одновременно, тем самым уменьшая общее время работы.
Параллелизм задач подчеркивает распределённый характер обработки (то есть потоков), в отличие от данных (параллелизм данных).
Параллелизм потоков – это параллелизм, характерный для приложений, которые задействуют одновременно несколько потоков. Этот тип широко распространён в приложениях, написанных для коммерческих серверов. Использование сразу нескольких потоков позволяет приложениям распределять нагрузки ввода-вывода данных: пока один поток ожидает доступа к памяти или диску, другие могут выполнять полезную работу.
1 Response
[…] Это — перевод статьи «Threading in Java» автора Beka Kodirov. Вторая часть статьи находится здесь. […]