Прогревочная
233 subscribers
11 photos
12 links
Java, Go и их рантаймы, многопоточность, конференции и немного IT-внутрянки. Прогреваемся разумно.

Null enjoyer: @lantsov_aleksander
Download Telegram
Channel photo updated
Привет!

Меня зовут Саша Ланцов, и в этом канале я хочу делиться своими мыслями, наблюдениями и находками о том, что вызвало у меня интерес и любопытство.

О чём будет канал:

Java и Go, их рантаймы и особенности

Долгое время я пишу на JVM-стеке: разрабатывал несколько low-latency алготрейдинговых систем для крупных инвестиционных банков.
Сейчас работаю в главной платёжной системе страны, где помимо прикладной разработки занимаюсь вопросами производительности расчётных платежных систем.

Стремительный рост Go тоже не оставляет равнодушным - насколько он соперничает с Java, в чём их отличия, и чему можно поучиться друг у друга?
Попробуем разобраться.



Многопоточное и конкурентное программирование

Ещё со времён учебы в институте я люблю эту тему. Помимо того, что часто сталкиваюсь с ней на работе - мне нравится про неё и рассказывать!

Например, вы могли видеть записи моих докладов с конференций - о применении нестандартных семантик для написания высокопроизводительного кода или об эволюции моделей памяти в разных языках.
Многопоточность с нами уже давно, но пробелов у разработчиков хватает. Постараемся заполнить их актуальными знаниями.

Спикерство, конференции и текущее состояние IT

Я убеждён: любой профессионал (а профессионалом я называю того, кто регулярно получает деньги за своё дело) всегда может чем-то поделиться.

Хочу рассказать о своём опыте участия в конференциях - как со стороны спикера, так и со стороны участника программного комитета. Мир конференций тесно связан с внутренней жизнью IT-компаний, так что интересные инсайты - будут.

А вообще… чего уж скрывать - периодически прогреваться тоже будем.
Поехали 🔥
👍8
Прогревочная pinned «Привет! Меня зовут Саша Ланцов, и в этом канале я хочу делиться своими мыслями, наблюдениями и находками о том, что вызвало у меня интерес и любопытство. О чём будет канал: Java и Go, их рантаймы и особенности Долгое время я пишу на JVM-стеке: разрабатывал…»
Собираюсь на Joker 2025

Прямо сейчас собираю вещи - завтра еду в Санкт-Петербург, где через пару дней стартует Joker 2025

В этом сезоне я впервые участвовал как член программного комитета (и, вот это удивительно, это съело совершенно неприличное количество времени!)
Поэтому на этот раз выступаю лишь с небольшим лайтнингом в конце первого дня.
Пока оставлю здесь скриншот - попробуйте угадать, про что он 👀

А теперь - немного про доклады, подготовку которых я сопровождал как член ПК:

🧵 Александр Маторин: "ThreadLocal устарел? Детальное сравнение со ScopedValue"
Внутрянка ScopedValue: откуда там взялась вероятностная структура данных а-ля фильтр Блума, и всегда ли ThreadLocal медленнее нового решения?
(спойлер: всё не так однозначно)

🔁 Дмитрий Фролов - "Ретраи: любовь с третьей попытки"
Концентрат базы по современной обработке ретраев. Даже опытные разработчики порой умудряются положить сервис retry storm-ами - а это та самая категория ошибок, которую нельзя недооценивать.

🦀 Денис Габайдулин - "Вызовите Rust - у нас тут медленно!"
Как подружить Java и Rust? Как прокидывать ошибки между языками, как перенаправить логирование из нативного кода в стандартный логгер?
Денис покажет практические подходы, которые реально работают, это очень интересный доклад.

UPD: Денис заболел и не смог выступить 😢

✈️ Алексей Рагозин: "JDK Flight Recorder в 2025-ом"
Доклады Алексея мне рекламировать не надо, тут и так все понятно. Будет и обзор эволюции JFR, и свежие фишки последнего релиза.
Имхо, добавилось много вкусного.

⚡️ Пётр Портнов - "Компилируем формулы в рантайме во имя перфа"
Про идеальный кейс применения JVM:
динамическая генерация кода в рантайме, который должен быстро работать после прогрева. Отличный баланс теории JIT и практических решений по производительности.

А вообще, если увидите меня на конфе - подходите, поболтаем 🙂
🔥10👍76
Новый GC в Go, async-profiler теперь в JDK и гнушно-сортировочные приколы

Привет! После Joker навалилось дел - не писал, исправляюсь 🙂
За минувшие две недели, помимо шашлычного спектакля, произошло несколько интересных событий



🍵 Новый сборщик мусора в Go

В Go 1.26 планируется переход на новый сборщик мусора Green Tea GC. Это эволюция классического concurrent mark-sweep-алгоритма, который обходит кучу в поисках живых объектов, но с важным отличием - теперь работа идет не по отдельным объектам, а целыми страницами (блоками памяти фиксированного размера, где эти объекты и хранятся). Это снижает накладные расходы и улучшает кэш-локальность. Также активно используются векторные SIMD-инструкции в процессе сканирования страниц. Некоторые уже попытались использовать этот сборщик, и результаты не столь однозначные; на мой взгляд вопрос состоит в том, насколько в реальности верна гипотеза об редкости слабозаполненных страниц с низкой плотностью живых объектов. Надо будет разобраться!

Мне особенно интересно сравнить подходы Go и Java. В OpenJDK все актуальные GC теперь используют гипотезы о поколениях (да, их несколько 😯) - и перемещают объекты в процессе своей работы. В Go все наоборот: GC не перемещает объекты, поколений нет, и куча не компактизируется.

Из этого вытекают интересные практические следствия: почему простой LRU-кэш в Java склонен к непотизму, а Go не страдает от фрагментации памяти? В общем, интересных вопросов тут много - постараюсь разобрать их в дальнейших постах.



🔍 async-profiler - новая версия 4.2 !

Я часто пользуюсь этим профилировщиком и рад, что он так активно развивается. В новой версии появился latency profiling mode. Например: хотим поймать редкий (но долгий) rehashing при добавлении значений в хэш-мапу - при обычном сэмплировании такое событие легко упустить, но новый режим решает эту проблему.

Помимо множества других улучшений - теперь async-profiler входит по умолчанию в состав Amazon Corretto JDK. Очень рад за авторов этого прекрасного инструмента и жду его появления в других JDK (ребята из Axiom JDK, привет 🤓)



🦀 Эпопея с заменой GNU Sort

На недавнем лайтнинге я рассказывал, как непросто корректно обрабатывать и сравнивать произвольные Unicode-строки. И вот снова беда.

В новых версиях Ubuntu классические GNU-утилиты постепенно заменяют на версии, переписанные на Rust. И тут обнаружилось: sort из проекта uutils ведёт себя иначе, чем оригинал - он игнорирует локаль при сортировке символов вроде “à”. Вроде бы мелочь, но такие расхождения потенциально ломают скрипты и CI-пайплайны.

Насколько сложно "один в один" воспроизвести старое и сложное поведение на новом стеке, даже если сам язык декларируется как более надежный и безопасный! Переписать - зачастую не проблема, а вот воссоздать десятилетия накопленных особенностей и зависимостей старого кода трудно. Увы, но таков путь.

На сегодня всё 😉
9🔥8💯6👍2🌚2
Доклад с конференции Т-Банка, планы на 2026 и полезности на новогодние каникулы

Привет! 2025 близок к завершению, в связи с чем хотел бы поделиться с уважаемыми подписчиками несколькими полезными вещами 🎁


Доклад с JVM Days

За окном уютно лежит свежий рыхлый снег - а ведь всего несколько месяцев назад были жаркие дни конца августа, когда Т-Банк и провел свою конференцию. Все записи уже доступны, здесь же я оставлю ссылку на свой доклад, в котором рассказываю не самые тривиальные (но и не сверх-специфичные!) способы оптимизации производительности на реальном примере одной платежной системы. Рассказываю про:

🔷 многопоточные конвейеры - как искать место для точечного ускорения

🔷 улучшенные способы вставки больших батчей в Postgres и волшебную функцию unnest

🔷 и немного про GC - на примере такого явления, как spiral of death, которое может выстреливать и в Java, и в Go, и вообще практически в любом рантайме со сборкой мусора


Планируемый доклад на SnowOne и подкаст "Тысяча фичей"

Сейчас мне интересна тема актуализации и +- адекватного сравнения алгоритмов сборки мусора и их реализаций в Java и Go; пока я все еще собираю и изучаю материалы

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

Тут у меня возникла проблема в том, что написание постов в ТГ-канале, кажется, не очень хорошо подходит именно для таких технических тем: есть существенные ограничения редактора, невозможность вставки картинок посреди текста и т.д. Пока так и не придумал формат именно серьезного технического поста (а не разгона). Выходов из этой ситуации сейчас вижу два

Во-первых, будет традиционный технический доклад на SnowOne / JPoint, на котором и будут разбираться данные вопросы. Ценовая политика у конференций разная: цена дороги до Нск + SnowOne вполне может оказаться дешевле, чем просто билет на JPoint; в любом случае, спустя ~полгода запись станет доступна всем

Во-вторых, уважаемый @apakhmv предложил мне записаться в январе на подкасте "Тысяча фичей", где я попробую рассказать про всё вот это только голосом - без каких-либо картинок и слайдов. Не знаю, удачно ли получится у меня, но сам подкаст интересный: рубать солёные огурцы для оливье под разбор внутрянки Cassandra - самое то, советую послушать на этих каникулах!

Преподавание в университете

Конец года принес мне авантюрное предложение от одного университета - сделать авторский курс по многопоточному программированию для студентов 2 курса, которые знают Go и классический Си

Авантюрное оно для меня, потому что я никогда раньше не преподавал (но всегда хотелось попробовать); лекции только-только начинают писаться, а в качестве практической работы я планирую предложить студентам написать свою простейшую реализацию LSM-based базы данных целиком в памяти - там есть большое количество шестеренок и механизмов, которые должны работать конкурентно друг с другом: начиная от потокобезопасных MemTable / SSTable и заканчивая компатизаторами и другими фоновыми механизмами

Курс будет на Go, с вкраплениями C, но не могу не поделиться прекрасным материалом от Одноклассников, который и вдохновил меня на эту идею: на курсе люди с нуля делают свою простую реализацию Cassandra на чистой Java. Чистое удовольствие!

На этом пока все. Поздравляю с наступающим Новым годом и Рождеством! До встречи в 2026🎄
Please open Telegram to view this post
VIEW IN TELEGRAM
👍11🔥64
пока искал иллюстрации для лекций, наткнулся на сайт, посвященный истории появления вычислительной техники с её первых дней. Конкретно эта фотография: программистка и её программа, 62 500 карточек, ~ 5 Мб, 1955 год

поздравляю с окончанием первой рабочей недели 📝
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5🔥54😁1
Forwarded from Тысяча фичей
61. Лучший GC: Java и Go.

Погружение в механизмы сборки мусора в Java и Go: исследование фундаментальных алгоритмов, эволюция от сборщиков с остановкой мира до конкурентных сборщиков, а также практические последствия для разработчиков. Mark-and-sweep, уплотнение памяти, гипотеза поколений и современные сборщики: G1, ZGC, Shenandoah и Green Tea GC в Go.

В гостях Саша Ланцов https://t.iss.one/pro_grevy
--

Авторский тгк Саши @toxic_enterprise
Альтер эго Саши @sashimi_pub
--

🎧 Слушать в Apple Podcasts | Spotify | Yutube | Яндекс | браузер
🔥103👍3
Тоже устали от бесконечного снега вокруг и уверены, что зима с нами ещё надолго?

Календарь подсказывает - весна уже через месяц, и в преддверии 8 марта коллеги из Spring АйО и Axiom JDK организуют митап, куда пригласили в том числе и меня - рассказать про Юникод и неочевидные грабли при его обработке. Буду рад видеть лично!

Когда: 05.03.2026, в 18.30 
Где: Москва, Лофт Casa Picassa, ул, Бауманская д.11, стр.8. Зал Кандинский
Бесплатно, но нужна регистрация

Подробная программа и регистрация:
https://java-rock-stars.timepad.ru/event/3814065/
11
Пока готовил очередной семинар, решил сделать небольшой развлекательный квиз по производительности 101 🥸

Даны две целочисленные матрицы a и b размером n * n; хотим подсчитать их произведение. Посмотрев определение матричного умножения можно сходу придумать что-то такое:

func multiply(a, b, c [][]int) {
n := len(a)
for i := 0; i < n; i++ {
for j := 0; j < n; j++ {
for k := 0; k < n; k++ {
c[i][j] += a[i][k] * b[k][j]
}
}
}
}


Может показаться неочевидным, но математический результат работы этой функции не зависит от того, в каком порядке записать все три цикла for; вот такая функция точно также в итоге умножает две матрицы:

func multiply_j_k_i(a, b, c [][]int) {
n := len(a)
for j := 0; j < n; j++ {
for k := 0; k < n; k++ {
for i := 0; i < n; i++ {
c[i][j] += a[i][k] * b[k][j]
}
}
}
}


То есть, существует шесть разных вариантов реализации данного алгоритма умножения, и они отличаются только порядком использования переменных i, j, k в циклах

Каждую реализацию можно обозначить в виде строки:
"i → j → k" - это первая функция multiply (цикл по i, затем вложенный цикл по j, затем вложенный по k), а "j → k → i" - функция multiply_j_k_i

Собственно, вопрос: а какая реализация из этих шести вариантов работает эффективнее?
Если есть объяснение почему - пишите в комментариях!
Пост с разбором разумеется будет
3👍2
Какая реализация умножения эффективнее?
Final Results
62%
i → j → k
31%
i → k → j
12%
j → i → k
12%
j → k → i
19%
k → i → j
15%
k → j → i
2
Разбор квиза 1/3: какая же из реализаций наивного алгоритма умножения матриц эффективнее?

Выбор метрики

Во-первых, как правильно заметили в комментариях, под эффективностью можно понимать разные вещи, например:

время исполнения
число выполненных инструкций на CPU
энергетическая эффективность (сколько энергии потратили на выполнение всей задачи)

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

Выбираем размер матрицы

Во-вторых, надо бы зафиксировать объем задачи, чтобы можно было сравнивать реализации: тут это просто размер матрицы n. Для маленьких матриц вся эта возня с индексами очевидно не будет интересной, поэтому пусть n = 1000

Простой бенчмарк

Пишем простейший go-бенчмарк и замеряем

Единственный момент, поскольку у нас присутствуют накладные расходы: начальное создание и заполнение матриц числами - это нужно правильно учесть в дальнейшем

Поэтому в бенчмарке есть NOP/baseline - матрицы не умножаются, но работа по подготовке матриц ведется

Результаты

Замеры делались на Intel i9-12900K
(для чистоты эксперимента энергоэффективные E-ядра отключены), и вот что получилось:

🥇 i → k → j : 784 ms (по опросу: 2)
🥈 k → i → j : 942 ms (по опросу: 3)

🥉 j → i → k : 1257 ms (по опросу: 5-6)
🥉 i → j → k : 1302 ms (по опросу: 1)

j → k → i : 1996 ms (по опросу: 5-6)
k → j → i : 2044 ms (по опросу: 4)

Почему-то i → k → j работает быстрее всех, фаворит опроса оказался середняком, а худшие варианты почти в 2,5 раза медленнее лучших.

В следующих постах разбираемся, как же так получилось👇
Please open Telegram to view this post
VIEW IN TELEGRAM
9
Разбор квиза 2/3: гипотеза о кэшах

Паттерны обращения к памяти

Заметим - обращение всегда идет по конкретным индексам:

🌀 матрица А: a[i][k]
🌀 матрица B: b[k][j]
🌀 матрица C: c[i][j]

Формула: c[i][j] += a[i][k] * b[k][j]

Давайте визуализируем паттерн этих обращений внутри самого вложенного цикла с помощью серого цвета

Например, для i → j → k последний вложенный цикл - по k, а значит в этом цикле:

💡чтение a[i][k] будет происходить в рамках строки i
💡чтение b[k][j] будет происходить в рамках столбца j
💡позиция элемента c[i][j] в рамках цикла по k фиксирована и не меняется

Кэши процессора

Если бы программа работала на абстрактной RA-машине (на чьей модели и строится теория анализа сложности алгоритмов) - паттерны доступа к памяти не имели бы никакого значения

Однако запускается то программа на реальном железе - в моем случае на x86. Между вычислительными устройствами процессора (они и исполняют инструкции) и памятью (в которой хранятся данные) находится иерархия из нескольких промежуточных кэшей: L1, L2, L3
CPU Core
|
v
L1 cache: ~32KB
|
v
L2 cache: ~256KB
|
v
L3 cache: ~10-30MB
|
v
RAM : гигабайты


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

Кэш-линии и их вытеснение

Когда происходит чтение области памяти по адресу address, и это значение ещё не хранится в кэше - происходит так называемый cache miss, в результате которого эта область и будет загружена в кэш - чтобы при дальнейшем обращении к ней доступ был быстрее. Все это происходит автоматически на уровне железа, и у прикладного разработчика нет возможности явно управлять этим механизмом

Кэш оперирует не отдельными адресами, а непрерывными областями памяти (обычно размером 64 байта, выравнивание такое же)

Когда происходит cache miss - в кэш загружается не только переменная по address, но и небольшая область памяти рядом с этой переменной - так называемая кэш-линия

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

Гипотеза

Теперь можно сформулировать теорию

Когда происходят доступы к элементам матрицы в рамках одной строки - загрузка кэш-линии помогает, чтения идут из этой же кэш-линии - это быстро 🔋

Когда доступы идут в рамках конкретного столбца - кэш-линия от текущего чтения никак будет использоваться при чтении следующего элемента столбца

Более того, чтение может вытеснять какую-то другую кэш-линию! Таким образом, мы гораздо хуже утилизируем кэш и работаем менее эффективно 🔋

Это объясняет почему i → k → j работает эффективнее: обращения идут построчно, что дает лучшую кэш-локальность

Но, конечно же, пока все это только теория. В следующем посте замеряем эффекты на практике 👇
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥61
Разбор квиза 3/3: проверка теории

В прошлой части мы сформулировали гипотезу:
порядок циклов влияет на локальность доступа к памяти → локальность влияет на кэш → кэш влияет на время выполнения

Теперь проверим это в реальности

Perf

Используем perf - стандартный Linux инструмент для анализа производительности. Для начала соберем существующий go-бенчмарк в виде бинарного файла:

go test -c -o bench.test


Для каждого варианта умножения запустим perf в режиме сбора событий:


perf stat -r 5 -e \
cpu_core/instructions/,\
cpu_core/cycles/,\
cpu_core/L1-dcache-loads/,\
cpu_core/L1-dcache-load-misses/ \
-- ./bench.test \
-test.run=^$ \
-test.bench='MatMul/IJK' \
-test.count=1 \
-test.benchtime=10x \
-n=1000


Что замеряем

В результате для каждого варианта получим результаты в таком формате:
 Performance counter stats for './bench.test -test.run=^$ -test.bench=MatMul/IJK -test.count=1 -test.benchtime=10x -n=1000' (5 runs):

297 353 515 594 cpu_core/instructions/ # 3,83 insn per cycle ( +- 0,00% )
77 606 222 836 cpu_core/cycles/ ( +- 0,21% )
108 062 152 268 cpu_core/L1-dcache-loads/ ( +- 0,00% )
10 936 805 980 cpu_core/L1-dcache-load-misses/ # 10,12% of all L1-dcache accesses ( +- 0,25% )


Где:
instructions - сколько инструкций реально выполнил CPU
cycles - cколько тактов процессора потребовалось
Из этих двух чисел выводится IPC (instruction-per-cycle) - количественная метрика, показывающая насколько эффективно наша программа работает с точки зрения CPU

L1-dcache-loads - количество попыток прочитать данные из L1-кэша
L1-dcache-load-misses - количество случаев, когда запрошенных данных не оказалось в L1 (пошли дальше по пути L2 → L3 → RAM)
Из этих двух чисел выводится, какая доля обращений к L1 оказалась cache-miss

Что получилось

Для каждой реализации я собрал соответствующие события, отнял baseline и отобразил на изображении

Для всех реализаций:
😀 количество инструкций почти одинаковое
😀 разница - в cycles и IPC
😀 разница коррелирует с L1 miss rate

У i → k → j:
меньше всего L1-промахов
выше IPC
меньше cycles

У k → j → i:
больше всего промахов
ниже IPC
больше cycles

Теория полностью совпала с практикой!

Важное уточнение

В прошлом посте мы рассматривали только самый внутренний цикл. Но тот же принцип работает и для промежуточного цикла. Именно поэтому, например, i → k → j и k → i → j тоже отличаются по результатам. Надо учитывать общий паттерн доступа к памяти!

Давайте подводить итоги👇
Please open Telegram to view this post
VIEW IN TELEGRAM
4
Квиз: итоги

Разумеется, никто в реальной жизни таким наивным алгоритмом матрицы не перемножает - есть куда более продвинутые техники. Если интересно, отличный разбор: Achieving Peak Performance for Matrix Multiplication in C++

Но цель этого упражнения была другой - показать, как одинаковый алгоритм с одинаковым количеством инструкций и одинаковой асимптотикой может давать разницу почти в 2,5 раза

Причина - кэши процессора и паттерны доступа к памяти

Ничего не меняя математически, просто начав ходить по памяти так, как это удобно железу - и все стало работать эффективнее

И это, по сути, и есть та самая mechanical sympathy - когда есть понимание, как система работает внутри, и не заставляешь ее делать то, что ей неудобно

Понравился ли вам данный лонгрид, делать ли еще такие, или что-то поменять в формате? Пишите в комментариях ❤️
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥202
SnowOne благополучно завершился, а ещё возврат из -25 в роскошные +3 весьма приятен!

С первым днем весны! 🥰
👍118🔥8