Upgrade to Pro — share decks privately, control downloads, hide ads and more …

[SnowOne 2025] Ильи Гаврилин: "Развивая процесс...

[SnowOne 2025] Ильи Гаврилин: "Развивая процессор: делаем JVM еще быстрее"

Современный микропроцессор — это не просто совокупность транзисторов, а уже настоящий «живой организм» со своими тайнами и парадоксами. В его конвейерной системе каждый такт имеет значение, а решения о будущем принимаются в доли наносекунд. Процессор пытается предсказывать будущее, изменяет сгенерированные инструкции, стараясь минимизировать простои и максимизировать производительность, хотя порой и ошибается.

Обсудим, как все эти «фокусы» улучшают производительность Java. Вы узнаете, как из кода можете влиять на поведение процессора, чего стоит избегать. И, наконец, вы убедитесь, что микроархитектура может влиять на исполнение кода. Мы также рассмотрим примеры, где подобные оптимизации дают реальный прирост, а где могут стать источником неожиданных проблем.

Видео: https://youtu.be/ADrKGEWGVig

Avatar for jugnsk

jugnsk

May 07, 2025
Tweet

More Decks by jugnsk

Other Decks in Programming

Transcript

  1. 2 Как я связан с процессором? Немного о себе: •

    Работаю в компании Syntacore c 2023 года • Занимался оптимизациями OpenJDK: • Округление FP значений • оптимизации математической библиотеки • Более полугода занимаюсь оптимизациями виртуальной машины JS в составе Chromium Илья Гаврилин Введение: зачем нам знания об архитектуре
  2. 3 Во что компилируется ваш код? • И снова знакомый

    “Hello World”: public class HelloWorld { public static void main(String[] args) { for (int i = 0; i < 300_000; i++) { // Method warmup helloWorld(); } } public static void helloWorld() { System.out.println("Hello, World!"); } } • Из него будет сгенерирован байткод • А что будет в конечном итоге? Введение: зачем нам знания об архитектуре
  3. 4 Во что компилируется ваш код? • И снова знакомый

    “Hello World”: {...} • Из него будет сгенерирован байткод : public class HelloWorld { public HelloWorld(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void helloWorld(); Code: 0: getstatic #13 // Field java/lang/System.out; 3: ldc #19 // String Hello, World! 5: invokevirtual #21 // Method java/io/PrintStream.println 8: return } • А что будет в конечном итоге? Введение: зачем нам знания об архитектуре
  4. 5 Во что компилируется ваш код? • И снова знакомый

    “Hello World”: {...} • Из него будет сгенерирован байткод : {...} • А что будет в конечном итоге: Address Bytes 0x..00 ff fe 72 b7 00 22 82 b3 0x..08 00 02 b0 23 fd 01 01 13 0x..10 02 81 30 23 02 11 34 23 0x..18 00 00 02 97 01 c2 e2 83 0x..20 02 0b e3 03 96 62 8a 63 0x..28 07 5d e3 17 00 1f c6 37 0x..30 62 06 06 13 00 b6 16 13 0x..38 01 56 06 13 00 66 16 13 0x..40 01 06 06 13 00 b3 93 93 0x..48 01 53 83 93 00 63 93 93 0x..50 01 03 83 93 00 00 33 37 0x..58 60 23 03 13 00 b3 13 13 Введение: зачем нам знания об архитектуре
  5. 6 Во что компилируется ваш код? • И снова знакомый

    “Hello World”: {...} • Из него будет сгенерирован байткод : {...} • А что будет в конечном итоге: 64 бита (DWORD) addi sp, sp, -48 1111 1101 0000 0001 0000 0001 0001 0011 Imm Rs Rd Введение: зачем нам знания об архитектуре Address Bytes 0x..00 ff fe 72 b7 00 22 82 b3 0x..08 00 02 b0 23 fd 01 01 13 0x..10 02 81 30 23 02 11 34 23 0x..18 00 00 02 97 01 c2 e2 83 0x..20 02 0b e3 03 96 62 8a 63 0x..28 07 5d e3 17 00 1f c6 37 0x..30 62 06 06 13 00 b6 16 13 0x..38 01 56 06 13 00 66 16 13 0x..40 01 06 06 13 00 b3 93 93 0x..48 01 53 83 93 00 63 93 93 0x..50 01 03 83 93 00 00 33 37 0x..58 60 23 03 13 00 b3 13 13
  6. 7 Во что компилируется ваш код? • И снова знакомый

    “Hello World”: {...} • Из него будет сгенерирован байткод : {...} • А что будет в конечном итоге: 64 бита (DWORD) addi sp, sp, -48 1111 1101 0000 0001 0000 0001 0001 0011 Imm Rs Rd • Для каждой архитектуры инструкции будут уникальны • Кажется, настало время выбрать архитектуру... Введение: зачем нам знания об архитектуре Address Bytes 0x..00 ff fe 72 b7 00 22 82 b3 0x..08 00 02 b0 23 fd 01 01 13 0x..10 02 81 30 23 02 11 34 23 0x..18 00 00 02 97 01 c2 e2 83 0x..20 02 0b e3 03 96 62 8a 63 0x..28 07 5d e3 17 00 1f c6 37 0x..30 62 06 06 13 00 b6 16 13 0x..38 01 56 06 13 00 66 16 13 0x..40 01 06 06 13 00 b3 93 93 0x..48 01 53 83 93 00 63 93 93 0x..50 01 03 83 93 00 00 33 37 0x..58 60 23 03 13 00 b3 13 13
  7. 8 Обратимся к истории • Доклад представлен более 10 лет

    назад, но не потерял актуальности. • Лишь через год после него была впервые представлена ISA RISC-V. • Все примеры из сегодняшнего доклада были запущены на архитектуре RISC-V. Введение: зачем нам знания об архитектуре Сергей Куксенко Сергей Куксенко
  8. 9 Extras: для тех, кто хочет знать всё Введение: зачем

    нам знания об архитектуре H&P Сh. 3 fdssdfsdf H&P Sixth edition
  9. 10 RISC-V: совершенство в простоте • За основу возьмём RV64G

    (G = IMAFDC_Zfencei_Zcsr) • Всего чуть больше 150 инструкций на которых JVM работает полноценно • На x86 нужно всего-то в 10 раз больше (>1600 инструкций) Введение: зачем нам знания об архитектуре
  10. 11 RISC: бывает не только V Введение: зачем нам знания

    об архитектуре • За основу возьмём RV64G (G = IMAFDC_Zfencei_Zcsr) • Всего чуть больше 150 инструкций на которых JVM работает полноценно • На x86 нужно всего-то в 10 раз больше (>1600 инструкций) • В случае aarch64 нам будет достаточно ~600 инструкций • Общие подходы RISC-V и aarch64 - схожи
  11. 12 RISC: бывает не только V Введение: зачем нам знания

    об архитектуре • За основу возьмём RV64G (G = IMAFDC_Zfencei_Zcsr) • Всего чуть больше 150 инструкций на которых JVM работает полноценно • На x86 нужно всего-то в 10 раз больше (>1600 инструкций) • В случае aarch64 нам будет достаточно ~600 инструкций • Общие подходы RISC-V и aarch64 - схожи • Почему не выбран привычный x86?
  12. 13 RISC: бывает не только V Введение: зачем нам знания

    об архитектуре • За основу возьмём RV64G (G = IMAFDC_Zfencei_Zcsr) • Всего чуть больше 150 инструкций на которых JVM работает полноценно • На x86 нужно всего-то в 10 раз больше (>1600 инструкций) • В случае aarch64 нам будет достаточно ~600 инструкций • Общие подходы RISC-V и aarch64 - схожи • Почему не выбран привычный x86? • Как прочитать такую инструкцию: PCLMULQDQ xmm1, xmm2/m128, imm8
  13. 14 RISC: бывает не только V Введение: зачем нам знания

    об архитектуре • За основу возьмём RV64G (G = IMAFDC_Zfencei_Zcsr) • Всего чуть больше 150 инструкций на которых JVM работает полноценно • На x86 нужно всего-то в 10 раз больше (>1600 инструкций) • В случае aarch64 нам будет достаточно ~600 инструкций • Общие подходы RISC-V и aarch64 - схожи • Почему не выбран привычный x86? • Как прочитать такую инструкцию: PCLMULQDQ xmm1, xmm2/m128, imm8 “Performs a carry-less multiplication of two quadwords, selected from the first source and second source operand according to the value of the immediate byte. Bits 4 and 0 are used to select which 64-bit half of each operand to use according to Table 4-13, other bits of the immediate byte are ignored.” Description:
  14. 15 Немного поговорим о терминах Введение: зачем нам знания об

    архитектуре • Архитектура процессора – описание логической модели процессора. • Микроархитектура – конкретная реализация архитектуры в процессоре. • ISA (Instruction Set Architecture) – набор инструкций, которые может выполнять процессор. • Длительность (Latency) – время выполнения одной инструкции, измеряемое в тактах. • IPC (Instructions Per Cycle) – количество инструкций, выполняемых за один такт. JVM возвышается над процессором
  15. 16 Немного поговорим о терминах Введение: зачем нам знания об

    архитектуре • Архитектура процессора – описание логической модели процессора. • Микроархитектура – конкретная реализация архитектуры в процессоре. • ISA (Instruction Set Architecture) – набор инструкций, которые может выполнять процессор. • Длительность (Latency) – время выполнения одной инструкции, измеряемое в тактах. • IPC (Instructions Per Cycle) – количество инструкций, выполняемых за один такт. • Такт - единица времени, за которую процессор исполняет базовое действие. • Связь с временем: tick[ns] = 1 / frequency[GHz] • Типичные длительности тактов: • freq = 1.5 GHz => tick = 0.(6) ns • freq = 3.5 GHz => tick ~ 0.29 ns • freq = 2.0 GHz => tick = 0.5 ns • Наш сегодняшний “подопытный”: LPi 4A
  16. 17 Микроархитектура: погрузимся глубже Транзисторный человек следит за током базы

    и регулирует выходной реостат для того, чтобы выходной ток был в h21Э больше тока базы В кремниевой тьме Транзисторы шепчут такт. Мир рождается. - Для тех, кто создавал процессоры Основы микроархитектуры: pipeline процессора
  17. 18 Микроархитектура: погрузимся глубже Fetch (F) Decode (D) Execute (Ex)

    Memory (Mem) (WB) • Fetch: Получение из памяти битов инструкции. • Decode: Декодирование инструкции. Подготовка Imm. Fetch регистров. • Execute: Исполнение инструкции исп. ALU, FPU, ... . • Memory: Сохрание результата в память. Чтение данных в регистр. • Writeback: Сохранение данных в регистры назначения. H&P App. C Основы микроархитектуры: pipeline процессора
  18. 19 Микроархитектура: погрузимся глубже Fetch (F) Decode (D) Execute (Ex)

    Memory (Mem) (WB) • Fetch: Получение из памяти битов инструкции. • Decode: Декодирование инструкции. Подготовка Imm. Fetch регистров. • Execute: Исполнение инструкции исп. ALU, FPU, ... . • Memory: Сохрание результата в память. Чтение данных в регистр. • Writeback: Сохранение данных в регистры назначения. 0x03010113 addi x2, x2, 48 x2 = 0x... x2 += 48 $x2 H&P App. C Основы микроархитектуры: pipeline процессора
  19. 20 Микроархитектура: погрузимся глубже Fetch (F) Decode (D) Execute (Ex)

    Memory (Mem) (WB) • Fetch: Получение из памяти битов инструкции. • Decode: Декодирование инструкции. Подготовка Imm. Fetch регистров. • Execute: Исполнение инструкции исп. ALU, FPU, ... . • Memory: Сохрание результата в память. Чтение данных в регистр. • Writeback: Сохранение данных в регистры назначения. H&P App. C Основы микроархитектуры: pipeline процессора
  20. 21 Время обновиться • Вы обновилили версию jdk, что же

    стоит сделать в первую очередь? Основы микроархитектуры: pipeline процессора
  21. 22 Время обновиться • Вы обновилили версию jdk, что же

    стоит сделать в первую очередь? • Замерим производительность: Benchmark JDK17(ops/ms) JDK21(ops/ms) Diff(%) acosDouble 28673 28859 0.65 CeilFloorDouble 38428 42241 9.92 CodeCacheStress 258 244 -5.60 FileChannelRead (size=100000) 288 286 -0.70 ArrayCopy.conjoint_micro (size = 8191) 540 544 0.73 Основы микроархитектуры: pipeline процессора
  22. 23 Время обновиться • Вы обновилили версию jdk, что же

    стоит сделать в первую очередь? • Замерим производительность: Benchmark Old JDK(ops/ms) New JDK(ops/ms) Diff(%) acosDouble 28673 28859 0.65 CeilFloorDouble 38428 42241 9.92 CodeCacheStress 258 244 -5.60 FileChannelRead (size=100000) 288 286 -0.70 ArrayCopy.conjoint_micro (size = 8191) 540 544 0.73 Основы микроархитектуры: pipeline процессора
  23. 24 Время обновиться • Вы обновилили версию jdk, что же

    стоит сделать в первую очередь? • Замерим производительность: Benchmark Old JDK(ops/ms) New JDK(ops/ms) Diff(%) acosDouble 28673 28859 0.65 CeilFloorDouble 38428 42241 9.92 CodeCacheStress 258 244 -5.60 FileChannelRead (size=100000) 288 286 -0.70 ArrayCopy.conjoint_micro (size = 8191) 540 544 0.73 • Почему снизилась производительность? $> java -XX:+PrintFlagsFinal -version ... bool UseRVA20U64 = true (similar to RISC-V C-ext) bool AvoidUnalignedAccesses = true bool UseUnalignedAccesses = false {diagnostic} ... Основы микроархитектуры: pipeline процессора
  24. 25 Время обновиться • Вы обновилили версию jdk, что же

    стоит сделать в первую очередь? • Замерим производительность: Benchmark Old JDK(ops/ms) New JDK(ops/ms) Diff(%) acosDouble 28673 28859 0.65 CeilFloorDouble 38428 42241 9.92 CodeCacheStress 258 244 -5.60 FileChannelRead (size=100000) 288 286 -0.70 ArrayCopy.conjoint_micro (size = 8191) 540 544 0.73 • Почему снизилась производительность? $> java -XX:+PrintFlagsFinal -version ... bool UseRVA20U64 = true (similar to RISC-V C-ext) bool AvoidUnalignedAccesses = true bool UseUnalignedAccesses = false {diagnostic} ... Основы микроархитектуры: pipeline процессора
  25. 26 C-ext: теперь меньше кода • Оптимизируем размер инструкции: addi

    sp, sp, -48 1111 1101 0000 0001 0000 0001 0001 0011 c.addi sp, -48 0000 0001 0100 0001 32 бита (WORD) 16 бит (HALF WORD) RISC-V: расширения
  26. 27 C-ext: теперь меньше кода • Оптимизируем размер инструкции: addi

    sp, sp, -48 1111 1101 0000 0001 0000 0001 0001 0011 c.addi sp, -48 0000 0001 0100 0001 32 бита (WORD) 16 бит (HALF WORD) • Теперь Fetch сложнее: RISC-V: расширения
  27. 28 C-ext: теперь меньше кода • Теперь Fetch сложнее: $>

    java -XX:+PrintFlagsFinal -version ... bool UseRVA20U64 = true (similar to RISC-V C-ext) bool AvoidUnalignedAccesses = true ( PC % 4 != 0 ) bool UseUnalignedAccesses = false {diagnostic} ... RISC-V: расширения
  28. 29 А что в реальных проектах? • Вы обновилили версию

    jdk, что же стоит сделать в первую очередь? • Замерим производительность: Benchmark Old JDK(ops/ms) New JDK(ops/ms) Diff(%) acosDouble 28673 28859 0.65 CeilFloorDouble 38428 42241 9.92 CodeCacheStress 258 244 -5.60 FileChannelRead (size=100000) 288 286 -0.70 ArrayCopy.conjoint_micro (size = 8191) 540 544 0.73 • Реальные приложения довольно редко модифицируют код (особенно после прогрева) • Наблюдается уменьшение размера CodeCache на 2-5% • Уменьшения производительности почти не наблюдается RISC-V: расширения
  29. 30 Execute: погрузимся глубже Fetch (F) Decode (D) Execute (Ex)

    Memory (Mem) (WB) • Fetch: Получение из памяти битов инструкции. • Decode: Декодирование инструкции. Подготовка Imm. Fetch регистров. • Execute: Исполнение инструкции исп. ALU, FPU, ... . • Memory: Сохрание результата в память. Чтение данных в регистр. • Writeback: Сохранение данных в регистры назначения. Основы микроархитектуры: pipeline процессора
  30. 31 Executor: не всё так просто • В современных процессорах

    разное число исполнителей: • Сложение/вычитание/... - 2 исполнителя • Умножение - 1 исполнитель • Деление - 1 исполнитель • Переходы - 1 исполнитель • Генерация адреса (LD + ST) - 1 исполнитель Основы микроархитектуры: Execute unit
  31. 32 Executor: не всё так просто • В современных процессорах

    разное число исполнителей: • Сложение/вычитание/... - 2 исполнителя • Умножение - 1 исполнитель • Деление - 1 исполнитель • Такой код можно исполнить быстрее: div x2, x2, 3 F D E E W mul x1, x1, 10 F D * E E W Основы микроархитектуры: Execute unit
  32. 33 div x2, x2, 3 F D E E W

    mul x1, x1, 10 F D * E E W Executor: не всё так просто • В современных процессорах разное число исполнителей: • Сложение/вычитание/... - 2 исполнителя • Умножение - 1 исполнитель • Деление - 1 исполнитель • Такой код можно исполнить быстрее: div x2, x2, 3 F D E E W mul x1, x1, 10 F D E E W • Но куда же можно потратить сэкономленную площадь кристала? - вернёмся к расширениям pipe 1 pipe 0 Основы микроархитектуры: Execute unit
  33. 34 RISC-V: расширения нужны, расширения важны • Однако, бывают и

    расширения, которые будут полезны в любом случае • Некоторые инструкции теперь могут быть реализованы прямо в кристалле • Допустим, вы захотели посчитать количество ведущих нулей в числе: RISC-V: расширения
  34. 35 RISC-V: расширения нужны, расширения важны • Однако, бывают и

    расширения, которые будут полезны в любом случае • Некоторые инструкции теперь могут быть реализованы прямо в кристалле • Допустим, вы захотели посчитать количество ведущих нулей в числе: const Type* CountLeadingZerosINode::Value(PhaseGVN* phase) const { if (ti && ti->is_con()) { jint i = ti->get_con(); // HD, Figure 5-6 if (i == 0) return TypeInt::make(BitsPerInt); int n = 1; // n - leading bits count unsigned int x = i; // i - input number if (x >> 16 == 0) { n += 16; x <<= 16; } if (x >> 24 == 0) { n += 8; x <<= 8; } if (x >> 28 == 0) { n += 4; x <<= 4; } if (x >> 30 == 0) { n += 2; x <<= 2; } n -= x >> 31; return TypeInt::make(n); } return TypeInt::INT; } 20 инструкций* = 20 тактов RISC-V: расширения
  35. 36 RISC-V: расширения нужны, расширения важны • Однако, бывают и

    расширения, которые будут полезны в любом случае • Некоторые инструкции теперь могут быть реализованы прямо в кристалле • Допустим, вы захотели посчитать количество ведущих нулей в числе: const Type* CountLeadingZerosINode::Value(PhaseGVN* phase) const { if (ti && ti->is_con()) { jint i = ti->get_con(); // HD, Figure 5-6 if (i == 0) return TypeInt::make(BitsPerInt); int n = 1; // n - leading bits count unsigned int x = i; // i - input number if (x >> 16 == 0) { n += 16; x <<= 16; } if (x >> 24 == 0) { n += 8; x <<= 8; } if (x >> 28 == 0) { n += 4; x <<= 4; } if (x >> 30 == 0) { n += 2; x <<= 2; } n -= x >> 31; return TypeInt::make(n); } return TypeInt::INT; } clz n, i; RISC-V: расширения
  36. 37 Ветвление: гроза любого процессора • Что если нам нужно

    сделать переход на специфический адрес ? Микроархитектура: branch-prediction j 0xDEADBEEF 0x6EFDB16F Addr: 0xDEADBEEF PC H&P App. C
  37. 38 Ветвление: гроза любого процессора • Что если нам нужно

    сделать переход на специфический адрес ? j 0xDEADBEEF 0x6EFDB16F Addr: 0xDEADBEEF PC blt t1, t2, 0xDEADBEEF ... Addr: 0xDEADBEEF Addr:... Addr: 0xDEADBEEF Cond: t1 < t2 Cond: + PC H&P Сh. 3 Микроархитектура: branch-prediction
  38. 39 Ветвление: попробуем угадать • Что если нам нужно сделать

    переход на специфический адрес ? blt t1, t2, 0xDEADBEEF ... Addr: 0xDEADBEEF Addr:... Addr: 0xDEADBEEF Cond: t1 < t2 Cond: + PC Cond: + (Maybe) Addr: 0xDEADBEEF PC blt t1, t2, 0xDEADBEEF H&P Сh. 3 Микроархитектура: branch-prediction
  39. 40 Ветвление: попробуем угадать • Что если нам нужно сделать

    переход на специфический адрес ? blt t1, t2, 0xDEADBEEF ... Addr: 0xDEADBEEF Addr:... Addr: 0xDEADBEEF Cond: t1 < t2 Cond: + PC Cond: + (Maybe) Addr: 0xDEADBEEF PC Цена ошибки blt t1, t2, 0xDEADBEEF Микроархитектура: branch-prediction
  40. 41 • Сравните два теста: Ветвление: ну на Java то

    это не важно //sortedArray - keep sorted @Benchmark public int testPredictableBranch(Blackhole bh) { int count = 0; for (int value : sortedArray) { if (value < IMPORTANT_CONSTANT) { count++; //some bussines logic } } return count; } //randomArray - leave as default @Benchmark public int testUnpredictableBranch(Blackhole bh){ int count = 0; for (int value : randomArray) { if (value < IMPORTANT_CONSTANT) { count++; //some bussines logic } } return count; } Микроархитектура: branch-prediction
  41. 42 • Сравните два теста: Ветвление: ну на Java то

    это не важно //sortedArray - keep sorted @Benchmark public int testPredictableBranch(Blackhole bh) { int count = 0; for (int value : sortedArray) { if (value < IMPORTANT_CONSTANT) { count++; //some bussines logic } } return count; } //randomArray - leave as default @Benchmark public int testUnpredictableBranch(Blackhole bh){ int count = 0; for (int value : randomArray) { if (value < IMPORTANT_CONSTANT) { count++; //some bussines logic } } return count; } Benchmark testPredictableBranch testUnpredictableBranch Score (ops/ms) 96.288 ± 0.381 41.032 ± 0.406 Микроархитектура: branch-prediction
  42. 43 Ветвление: не нужно ли нам подсказывать процессору? • В

    процессе компиляции мы действительно прогнозируем переходы: $> java -XX:+UnlockDiagnosticVMOptions -XX:+PrintOptCode ... ... 0b8 B5 : out( B31 B6 ) <- in( B4 B8 ) Loop Freq: 1.99997 110 B14: out( B23 B15 ) <- in( B13 B22 ) top-of-loop Freq: 9.97272e+09 ... Микроархитектура: branch-prediction
  43. 44 Ветвление: не нужно ли нам подсказывать процессору? • В

    процессе компиляции мы действительно прогнозируем переходы: $> java -XX:+UnlockDiagnosticVMOptions -XX:+PrintOptCode ... ... 0b8 B5 : out( B31 B6 ) <- in( B4 B8 ) Loop Freq: 1.99997 110 B14: out( B23 B15 ) <- in( B13 B22 ) top-of-loop Freq: 9.97272e+09 ... • На некоторых процессорах нам дают возможность использовать наши знания*: disp ctp_reg, label [, ipd NUM] Description: • ctp_reg - регистр подготовки перехода; • label - метка целевого адреса; • NUM - необязательный параметр, глубина подкачки кода в терминах количества строк L1$I ; NUM = 0, 1, 2. * МЦСТ “Эльбрус”: Операции подготовки передачи управления Микроархитектура: branch-prediction
  44. 45 Ветвление: не нужно ли нам подсказывать процессору? • В

    процессе компиляции мы действительно прогнозируем переходы: $> java -XX:+UnlockDiagnosticVMOptions -XX:+PrintOptCode ... ... 0b8 B5 : out( B31 B6 ) <- in( B4 B8 ) Loop Freq: 1.99997 110 B14: out( B23 B15 ) <- in( B13 B22 ) top-of-loop Freq: 9.97272e+09 ... • На некоторых процессорах нам дают возможность использовать наши знания: • Однако, большинство процессоров сегодня изпользуют механизм branch-prediction disp ctp_reg, label [, ipd NUM] Description: • ctp_reg - регистр подготовки перехода; • label - метка целевого адреса; • NUM - необязательный параметр, глубина подкачки кода в терминах количества строк L1$I ; NUM = 0, 1, 2. Микроархитектура: branch-prediction
  45. 46 Ветвление: branch-predictor не так то прост • Простейший branch-predictor,

    “знакомый со школы”: счётчик с насыщением • Хорошо предсказывает однозначные переходы • Крайне прост в реализации, работает мгновенно 2-bit saturating counter Микроархитектура: branch-prediction
  46. 47 Ветвление: branch-predictor не так то прост • Простейший branch-predictor,

    “знакомый со школы”: счётчик с насыщением • Хорошо предсказывает однозначные переходы • Крайне прост в реализации, работает мгновенно • Не учитывает глобальную историю переходов • Не находит паттерны в срабатывании переходов 2-bit saturating counter Микроархитектура: branch-prediction
  47. 48 Ветвление: branch-predictor не так то прост • Пример реального

    предсказателя: Bi-Mode branch-predictor (Thead C910) • Опирается на глобальную историю • Сочетает различные подходы к предсказанию перехода The Bi-Mode Branch Predictor Микроархитектура: branch-prediction
  48. 49 Ветвление: branch-predictor не так то прост • Пример реального

    предсказателя: Bi-Mode branch-predictor (Thead C910) • Опирается на глобальную историю • Сочетает различные подходы к предсказанию перехода Основные выводы: • Для предсказания важен адрес инструкции • После деоптимизации алгоритм будет обучаться заново • Inline однотипной функции не ухудшит предсказания • Компактность кода улучшит предсказания Микроархитектура: branch-prediction
  49. 50 Ветвление: всегда ли вам нужен полноценный переход? //randomArray -

    leave as default @Benchmark public int testUnpredictableBranch(Blackhole bh){ for (int i = 0; i < randomArray.length; i++) { if (arr[i] > IMPORTANT_CONSTANT) { // Huge size could not be inlined complicatedFunction(arr[i]); } } return count; } • Возможно, вы делаете достаточно сложную операцию: Микроархитектура: branch-prediction
  50. 51 Ветвление: всегда ли вам нужен полноценный переход? //randomArray -

    leave as default @Benchmark public int testUnpredictableBranch(Blackhole bh){ for (int i = 0; i < randomArray.length; i++) { if (arr[i] > IMPORTANT_CONSTANT) { // Huge size could not be inlined complicatedFunction(arr[i]); } } return count; } • Возможно, вы делаете достаточно сложную операцию: 1. save regs 2. prepare args (arr[i]) 3. PC = &complicated... 4. restore regs Микроархитектура: branch-prediction
  51. 52 Ветвление: всегда ли вам нужен полноценный переход? //randomArray -

    leave as default @Benchmark public int testUnpredictableBranch(Blackhole bh){ for (int i = 0; i < randomArray.length; i++) { if (arr[i] > IMPORTANT_CONSTANT) { arr[i] = 0; } } return count; } • Возможно, вы делаете достаточно простую операцию: Микроархитектура: branch-prediction
  52. 53 Ветвление: всегда ли вам нужен полноценный переход? //randomArray -

    leave as default @Benchmark public int testUnpredictableBranch(Blackhole bh){ for (int i = 0; i < randomArray.length; i++) { if (arr[i] > IMPORTANT_CONSTANT) { // branch + cmp - 2 tick arr[i] = 0; // store - 1 tick } } return count; } • Возможно, вы делаете достаточно простую операцию: • Давайте посчитаем сколько нам будет стоить этот манёвр • Рассмотрим Apple M1 (Firestorm) и длительности исполнения инструкций github.com/ insn_bench_aarch64 Микроархитектура: branch-prediction
  53. 54 Ветвление: всегда ли вам нужен полноценный переход? //randomArray -

    leave as default @Benchmark public int testUnpredictableBranch(Blackhole bh){ for (int i = 0; i < randomArray.length; i++) { if (arr[i] > IMPORTANT_CONSTANT) { // branch + cmp - 8 tick arr[i] = 0; // store - 1 tick } } return count; } • Возможно, вы делаете достаточно простую операцию: • Давайте посчитаем сколько нам будет стоить этот манёвр • Рассмотрим Apple M1 (Firestorm) и длительности исполнения инструкций github.com/ insn_bench_aarch64 Микроархитектура: branch-prediction
  54. 55 Ветвление: instruction you didn’t know you wanted • Мы

    хотим “условное перемещение”: conditional move • Затрагивает только стадию Execution • Не зависит от branch-predictor • Почти бесплатен: 1 такт на большинстве CPU Assembly <=> Pseudo-code csel Xd, Xn, Xm, cond <=> Xd = (cond == true)? Xn : Xm; csinc Xd, Xn, Xm, cond <=> Xd = (cond == true)? Xn : (Xm + 1); “csinc” - instruction you didn`t know... Микроархитектура: branch-prediction
  55. 56 Ветвление: instruction you didn’t know you wanted • Мы

    хотим “условное перемещение”: conditional move • Затрагивает только стадию Execution • Не зависит от branch-predictor • Почти бесплатен: 1 такт на большинстве CPU Assembly <=> Pseudo-code csel Xd, Xn, Xm, cond <=> Xd = (cond == true)? Xn : Xm; csinc Xd, Xn, Xm, cond <=> Xd = (cond == true)? Xn : (Xm + 1); • Не забыли и про RISC-V: Zicond Assembly <=> Pseudo-code czero.eqz rd, rs1, cond <=> rd = (cond == zero)? 0 : rs1; czero.nez rd, rs1, cond <=> rd = (cond != zero)? 0 : rs1; Микроархитектура: branch-prediction
  56. 57 Ветвление: всегда ли вам нужен полноценный переход? QUASI ARCHITECTO

    BEATAE //randomArray - leave as default @Benchmark public int testUnpredictableBranch(Blackhole bh){ for (int i = 0; i < randomArray.length; i++) { if (arr[i] > IMPORTANT_CONSTANT) { arr[i] = 0; } } return count; } • Возможно, вы делаете достаточно простую операцию: • Давайте посчитаем сколько нам будет стоить этот манёвр • Рассмотрим Apple M1 (Firestorm) и длительности исполнения инструкций 0x44: ldr w13, [x10, #16] 0x48: cmp w13, #0x32 0x4c: b.le 0x54 ;*if_icmple(...) 0x50: str wzr, [x10, #16] 0x54: ....
  57. 58 Ветвление: всегда ли вам нужен полноценный переход? //randomArray -

    leave as default @Benchmark public int testUnpredictableBranch(Blackhole bh){ for (int i = 0; i < randomArray.length; i++) { if (arr[i] > IMPORTANT_CONSTANT) { arr[i] = 0; } } return count; } • Возможно, вы делаете достаточно простую операцию: • Давайте посчитаем сколько нам будет стоить этот манёвр • Рассмотрим Apple M1 (Firestorm) и длительности исполнения инструкций 0x44: ldr w13, [x10, #16] 0x48: cmp w13, #0x32 0x4c: b.le 0x54 ;*if_icmple(...) 0x50: str wzr, [x10, #16] 0x54: .... Микроархитектура: branch-prediction
  58. 59 Ветвление: а можно оно будет работать? • Применим секретное

    знание: заглянем в код JVM Node *PhaseIdealLoop::conditional_move( Node *region ) { // Check for CFG diamond Node *lp = region->in(1); Node *rp = region->in(2); if (!lp || !rp) return nullptr; // Check for ops pinned in an arm of the diamond. // Can't remove the control flow in this case if (lp->outcnt() > 1) return nullptr; if (rp->outcnt() > 1) return nullptr; // Always convert to CMOVE if all results are used only outside this loop. bool used_inside_loop = (r_loop == _ltree_root); // Check profitability ... Микроархитектура: branch-prediction
  59. 60 Ветвление: а можно оно будет работать? • Применим секретное

    знание: заглянем в код JVM Node *PhaseIdealLoop::conditional_move( Node *region ) { // 1. Check for CFG diamond // 2. Check for ops pinned in an arm of the diamond. // 3. Always convert to CMOVE if all results are used only outside this loop. // 4. Check profitability • Посмотрим внимательнее на наш код: if (arr[i] > IMPORTANT_CONSTANT) { arr[i] = 0; } Микроархитектура: branch-prediction
  60. 61 Ветвление: а можно оно будет работать? • Применим секретное

    знание: заглянем в код JVM Node *PhaseIdealLoop::conditional_move( Node *region ) { // 1. Check for CFG diamond // 2. Check for ops pinned in an arm of the diamond. // 3. Always convert to CMOVE if all results are used only outside this loop. // 4. Check profitability • Посмотрим внимательнее на наш код: arr[i] = (arr[i] > IMPORTANT_CONSTANT)? 0 : arr[i] Микроархитектура: branch-prediction
  61. 62 Ветвление: а можно оно будет работать? //randomArray - leave

    as default @Benchmark public int testUnpredictableBranch(Blackhole bh){ for (int i = 0; i < randomArray.length; i++) { arr[i] = (arr[i] > IMPORTANT_CONSTANT) ? 0 : arr[i]; } return count; } • Возможно, вы делаете достаточно простую операцию: ldrw x13, [x11, #16] # int cmpw x13, #50 cselw x13, zr, x13 gt # int strw x13, [x11, #16] # int .... • Но мы избавились далеко не от всех проблем • Новые вызовы: инструкций стало больше, появилось много store Микроархитектура: branch-prediction
  62. 63 Ветвление: а можно оно будет работать? • Новые проблемы:

    ldrw x13, [x11, #16] cmpw x13, #50 cselw x13, zr, x13 gt strw x13, [x11, #16] ldrw x12, [x11, #20] cmpw x12, #50 cselw x12, zr, x12 gt strw x12, [x11, #20] ldrw x13, [x11, #24] cmpw x13, #50 cselw x13, zr, x13 gt strw x13, [x11, #24] ldrw x12, [x11, #28] cmpw x12, #50 cselw x12, zr, x12 gt strw x12, [x11, #28] Микроархитектура: branch-prediction
  63. 64 Ветвление: а можно оно будет работать? • Новые проблемы:

    ldrw x13, [x11, #16] cmpw x13, #50 cselw x13, zr, x13 gt strw x13, [x11, #16] ldrw x12, [x11, #20] cmpw x12, #50 cselw x12, zr, x12 gt strw x12, [x11, #20] ldrw x13, [x11, #24] cmpw x13, #50 cselw x13, zr, x13 gt strw x13, [x11, #24] ldrw x12, [x11, #28] cmpw x12, #50 cselw x12, zr, x12 gt strw x12, [x11, #28] Инструкций - больше Переходов - меньше Процессор не успевает Микроархитектура: branch-prediction
  64. 65 Ветвление: а можно оно будет работать? Современный процессор: OoO

    и superscalar • Новые проблемы: ldrw x13, [x11, #16] cmpw x13, #50 cselw x13, zr, x13 gt strw x13, [x11, #16] ldrw x12, [x11, #20] cmpw x12, #50 cselw x12, zr, x12 gt strw x12, [x11, #20] ldrw x13, [x11, #24] cmpw x13, #50 cselw x13, zr, x13 gt strw x13, [x11, #24] ldrw x12, [x11, #28] cmpw x12, #50 cselw x12, zr, x12 gt strw x12, [x11, #28] Зависят по данным (x12) от ldrw Не может быть выполнена раньше strw
  65. 66 Попробуем оптимизировать микроархитектуру • Всё таки иногда наш конвеер

    простаивает: Fetch (F) Decode (D) Execute (Ex) Memory (Mem) (WB) lw a0, 0(t0) F D E M M M W addi a1, a1, 1 F D * * * E W addi a2, a2, 2 F D * * * E W add a3, a0, a1 F D * * * E W H&P Сh. 3 Современный процессор: OoO и superscalar
  66. 67 Реализуем Out of Order (OoO) Fetch (F) Decode (D)

    Memory (Mem) (WB) • Всё таки иногда наш конвеер простаивает: H&P Сh. 3 lw a0, 0(t0) F D E M M M W addi a1, a1, 1 F D E W addi a2, a2, 2 F D E W add a3, a0, a1 F D * E W Современный процессор: OoO и superscalar
  67. 68 Реализуем Out of Order (OoO) Fetch (F) Decode (D)

    Memory (Mem) (WB) • Всё таки иногда наш конвеер простаивает: H&P Сh. 3 lw a0, 0(t0) F D E M M M W addi a1, a1, 1 F D E W addi a2, a2, 2 F D E W add a3, a0, a1 F D * E W Современный процессор: OoO и superscalar
  68. 69 Мы больше не нуждаемся в услугах JVM H&P Сh.

    3 $> java -XX:+UnlockDiagnosticVMOptions -XX:-OptoScheduling .... • На моём железе (LPi 4A) глубина ROB - 192 инструкции • В случае Apple M1 - более 330* Современный процессор: OoO и superscalar
  69. 70 Мы больше не нуждаемся в услугах JVM H&P Сh.

    3 $> java -XX:+UnlockDiagnosticVMOptions -XX:-OptoScheduling .... Имя теста No scheduling With scheduling Разница (%) StrictMathBench.floorDouble 48052.698 48055.694 0.006% StrictMathBench.floorDivLongLong 52219.027 52020.464 0.38% StrictMathBench.nextDownDouble 47198.052 47218.959 0.04% StrictMathBench.nextAfterFloatDouble 48035.984 48094.173 0.12% • Осторожно применяйте данную опцию, результат может быть непредсказуем • Тестируйте в каждой связке: проект + процессор • На моём железе (LPi 4A) глубина ROB - 192 инструкции • В случае Apple M1 - более 330* Современный процессор: OoO и superscalar
  70. 71 Реализуем Out of Order (OoO) Fetch (F) Decode (D)

    Memory (Mem) (WB) • Всё таки иногда наш конвеер простаивает: H&P Сh. 3 lw a0, 0(t0) F D E M M M W addi a1, a1, 1 F D E W addi a2, a2, 2 F D E W add a3, a0, a1 F D * E W Современный процессор: OoO и superscalar
  71. 72 Наш процессор: теперь суперскалярный Fetch (F) Decode (D) Memory

    (Mem) (WB) X2 • Конвеер больше не простаивает: H&P Сh. 3 lw a0, 0(t0) F D E M M M W addi a1, a1, 1 F D E W addi a2, a2, 2 F D E W add a3, a0, a1 F D * * * E W Современный процессор: OoO и superscalar
  72. 73 Наш процессор: теперь суперскалярный Fetch (F) Decode (D) Memory

    (Mem) (WB) • Конвеер больше не простаивает: H&P Сh. 3 lw a0, 0(t0) F D E M M M W addi a1, a1, 1 F D E W addi a2, a2, 2 F D E W add a3, a0, a1 F D * * * E W X2 Современный процессор: OoO и superscalar
  73. 74 Оцените разницу • Конвеер стал эффективнее: • Всё таки

    иногда наш конвеер простаивает: H&P Сh. 3 lw a0, 0(t0) F D E M M M W addi a1, a1, 1 F D E W addi a2, a2, 2 F D E W add a3, a0, a1 F D * * * E W lw a0, 0(t0) F D E M M M W addi a1, a1, 1 F D * * * E W addi a2, a2, 2 F D * * * E W add a3, a0, a1 F D * * * E W Современный процессор: OoO и superscalar
  74. 75 Подсказываем JVM про наш процессор H&P Сh. 3 •

    Суперскалярность бывает разной: • LPi 4A: dual-issue • Apple M1 (Firestorm): octa-issue • Можно настроить JVM: Flags [default]: LoopMaxUnroll [16] Maximum number of unrolls for main loop LoopUnrollMin [ 4] Minimum number of unroll loop bodies before checking progress of rounds of unroll,optimize • P.S. для разработчиков JVM: обращайте внимание на переменные unroll_factor. • Изменяйте параметры с особой осторожностью • Ставьте значения кратные/похожие на issue-width вашего целевого процессора Современный процессор: OoO и superscalar
  75. 76 Адаптируем JVM под современный процессор • Ведётся работа над

    JEP: https://bugs.openjdk.org/browse/JDK-8279184 В планах: • Уменшить размер CodeCache • Разделять методы по самым горячим участкам • Уменьшение разницы в виртуальных адресах Instruction Issue Cache Современный процессор: OoO и superscalar
  76. 77 Адаптируем JVM под современный процессор • Ведётся работа над

    JEP: https://bugs.openjdk.org/browse/JDK-8279184 В планах: • Уменшить размер CodeCache • Разделять методы по самым горячим участкам • Уменьшение разницы в виртуальных адресах Современный OoO Octa-issue процессор Instruction Issue Cache Современный процессор: OoO и superscalar
  77. 78 Как изменилась наша схема Процессор: подводим итоги • С

    чего мы начинали: • Уже способна исполнять все инструкции • Single-issue - загружает мало инструкций • In-order с простейшим Executor
  78. 79 Как изменилась наша схема Процессор: подводим итоги • Уже

    способна исполнять все инструкции • Dual-issue - в два раза больше инструкций • Out-of-Order с ROB на сотни инструкций X2
  79. 80 Выводы • Не все расширения одинаково полезны - с

    каждым нужно разбираться отдельно • Не пытайтесь обмануть branch-predictor, он всё предскажет сам • Любите алмазы, особенно, если они в CFG • Экспериментируйте с оптимизациями в C2, но делайте это осторожно Процессор: подводим итоги Полезные ссылки • RISC-V spec: https://riscv.org/specifications/ratified/ • TH C910 overview: https://chipsandcheese.com/p/alibabat-heads-xuantie-c910 • Apple M1 overview: https://dougallj.github.io/applecpu/firestorm.html • aarch64 instruction latencies: https://github.com/ocxtal/insn_bench_aarch64 • RISC-V instruction DB: https://github.com/riscv-software-src/riscv-unified-db
  80. 81 Тесты: вернёмся к истокам QUASI ARCHITECTO BEATAE Test Average

    (ops/ms) DoubleSumThroughput.test1 293.457 DoubleSumThroughput.manualUnroll 673.248 • Остались ли тесты из доклада Сергея Куксенко актуальными спустя 10 лет? @Benchmark @OperationsPerInvocation(SIZE) public double test1() { double sum = 0.0; for (int i = 0; i < LEN; i++) { sum += array[i]; } return sum; } @Benchmark @OperationsPerInvocation(SIZE) public double manualUnroll() { double sum = 0.0; for (int i = 0; i < LEN; i += 4) { sum += array[i] + array[i + 1] + array[i + 2] + array[i + 3]; } return sum; }