Привет!
Меня зовут Саша Ланцов, и в этом канале я хочу делиться своими мыслями, наблюдениями и находками о том, что вызвало у меня интерес и любопытство.
О чём будет канал:
Java и Go, их рантаймы и особенности
Долгое время я пишу на JVM-стеке: разрабатывал несколько low-latency алготрейдинговых систем для крупных инвестиционных банков.
Сейчас работаю в главной платёжной системе страны, где помимо прикладной разработки занимаюсь вопросами производительности расчётных платежных систем.
Стремительный рост Go тоже не оставляет равнодушным - насколько он соперничает с Java, в чём их отличия, и чему можно поучиться друг у друга?
Попробуем разобраться.
Многопоточное и конкурентное программирование
Ещё со времён учебы в институте я люблю эту тему. Помимо того, что часто сталкиваюсь с ней на работе - мне нравится про неё и рассказывать!
Например, вы могли видеть записи моих докладов с конференций - о применении нестандартных семантик для написания высокопроизводительного кода или об эволюции моделей памяти в разных языках.
Многопоточность с нами уже давно, но пробелов у разработчиков хватает. Постараемся заполнить их актуальными знаниями.
Спикерство, конференции и текущее состояние IT
Я убеждён: любой профессионал (а профессионалом я называю того, кто регулярно получает деньги за своё дело) всегда может чем-то поделиться.
Хочу рассказать о своём опыте участия в конференциях - как со стороны спикера, так и со стороны участника программного комитета. Мир конференций тесно связан с внутренней жизнью IT-компаний, так что интересные инсайты - будут.
А вообще… чего уж скрывать - периодически прогреваться тоже будем.
Поехали 🔥
Меня зовут Саша Ланцов, и в этом канале я хочу делиться своими мыслями, наблюдениями и находками о том, что вызвало у меня интерес и любопытство.
О чём будет канал:
Java и Go, их рантаймы и особенности
Долгое время я пишу на JVM-стеке: разрабатывал несколько low-latency алготрейдинговых систем для крупных инвестиционных банков.
Сейчас работаю в главной платёжной системе страны, где помимо прикладной разработки занимаюсь вопросами производительности расчётных платежных систем.
Стремительный рост Go тоже не оставляет равнодушным - насколько он соперничает с Java, в чём их отличия, и чему можно поучиться друг у друга?
Попробуем разобраться.
Многопоточное и конкурентное программирование
Ещё со времён учебы в институте я люблю эту тему. Помимо того, что часто сталкиваюсь с ней на работе - мне нравится про неё и рассказывать!
Например, вы могли видеть записи моих докладов с конференций - о применении нестандартных семантик для написания высокопроизводительного кода или об эволюции моделей памяти в разных языках.
Многопоточность с нами уже давно, но пробелов у разработчиков хватает. Постараемся заполнить их актуальными знаниями.
Спикерство, конференции и текущее состояние IT
Я убеждён: любой профессионал (а профессионалом я называю того, кто регулярно получает деньги за своё дело) всегда может чем-то поделиться.
Хочу рассказать о своём опыте участия в конференциях - как со стороны спикера, так и со стороны участника программного комитета. Мир конференций тесно связан с внутренней жизнью IT-компаний, так что интересные инсайты - будут.
А вообще… чего уж скрывать - периодически прогреваться тоже будем.
Поехали 🔥
👍8
Прогревочная pinned «Привет! Меня зовут Саша Ланцов, и в этом канале я хочу делиться своими мыслями, наблюдениями и находками о том, что вызвало у меня интерес и любопытство. О чём будет канал: Java и Go, их рантаймы и особенности Долгое время я пишу на JVM-стеке: разрабатывал…»
Собираюсь на Joker 2025
Прямо сейчас собираю вещи - завтра еду в Санкт-Петербург, где через пару дней стартует Joker 2025
В этом сезоне я впервые участвовал как член программного комитета (и, вот это удивительно, это съело совершенно неприличное количество времени!)
Поэтому на этот раз выступаю лишь с небольшим лайтнингом в конце первого дня.
Пока оставлю здесь скриншот - попробуйте угадать, про что он 👀
А теперь - немного про доклады, подготовку которых я сопровождал как член ПК:
🧵 Александр Маторин: "ThreadLocal устарел? Детальное сравнение со ScopedValue"
Внутрянка
(спойлер: всё не так однозначно)
🔁 Дмитрий Фролов - "Ретраи: любовь с третьей попытки"
Концентрат базы по современной обработке ретраев. Даже опытные разработчики порой умудряются положить сервис
🦀 Денис Габайдулин - "Вызовите Rust - у нас тут медленно!"
Как подружить Java и Rust? Как прокидывать ошибки между языками, как перенаправить логирование из нативного кода в стандартный логгер?
Денис покажет практические подходы, которые реально работают, это очень интересный доклад.
UPD: Денис заболел и не смог выступить 😢
✈️ Алексей Рагозин: "JDK Flight Recorder в 2025-ом"
Доклады Алексея мне рекламировать не надо, тут и так все понятно. Будет и обзор эволюции JFR, и свежие фишки последнего релиза.
Имхо, добавилось много вкусного.
⚡️ Пётр Портнов - "Компилируем формулы в рантайме во имя перфа"
Про идеальный кейс применения JVM:
динамическая генерация кода в рантайме, который должен быстро работать после прогрева. Отличный баланс теории JIT и практических решений по производительности.
А вообще, если увидите меня на конфе - подходите, поболтаем 🙂
Прямо сейчас собираю вещи - завтра еду в Санкт-Петербург, где через пару дней стартует Joker 2025
В этом сезоне я впервые участвовал как член программного комитета (и, вот это удивительно, это съело совершенно неприличное количество времени!)
Поэтому на этот раз выступаю лишь с небольшим лайтнингом в конце первого дня.
Пока оставлю здесь скриншот - попробуйте угадать, про что он 👀
А теперь - немного про доклады, подготовку которых я сопровождал как член ПК:
🧵 Александр Маторин: "ThreadLocal устарел? Детальное сравнение со ScopedValue"
Внутрянка
ScopedValue: откуда там взялась вероятностная структура данных а-ля фильтр Блума, и всегда ли ThreadLocal медленнее нового решения?(спойлер: всё не так однозначно)
🔁 Дмитрий Фролов - "Ретраи: любовь с третьей попытки"
Концентрат базы по современной обработке ретраев. Даже опытные разработчики порой умудряются положить сервис
retry storm-ами - а это та самая категория ошибок, которую нельзя недооценивать.Как подружить Java и Rust? Как прокидывать ошибки между языками, как перенаправить логирование из нативного кода в стандартный логгер?
Денис покажет практические подходы, которые реально работают, это очень интересный доклад.
UPD: Денис заболел и не смог выступить 😢
✈️ Алексей Рагозин: "JDK Flight Recorder в 2025-ом"
Доклады Алексея мне рекламировать не надо, тут и так все понятно. Будет и обзор эволюции JFR, и свежие фишки последнего релиза.
Имхо, добавилось много вкусного.
⚡️ Пётр Портнов - "Компилируем формулы в рантайме во имя перфа"
Про идеальный кейс применения JVM:
динамическая генерация кода в рантайме, который должен быстро работать после прогрева. Отличный баланс теории JIT и практических решений по производительности.
А вообще, если увидите меня на конфе - подходите, поболтаем 🙂
🔥10👍7❤6
Новый 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 не страдает от фрагментации памяти? В общем, интересных вопросов тут много - постараюсь разобрать их в дальнейших постах.
🔍
Я часто пользуюсь этим профилировщиком и рад, что он так активно развивается. В новой версии появился latency profiling mode. Например: хотим поймать редкий (но долгий)
Помимо множества других улучшений - теперь
🦀 Эпопея с заменой GNU Sort
На недавнем лайтнинге я рассказывал, как непросто корректно обрабатывать и сравнивать произвольные Unicode-строки. И вот снова беда.
В новых версиях Ubuntu классические GNU-утилиты постепенно заменяют на версии, переписанные на Rust. И тут обнаружилось:
Насколько сложно "один в один" воспроизвести старое и сложное поведение на новом стеке, даже если сам язык декларируется как более надежный и безопасный! Переписать - зачастую не проблема, а вот воссоздать десятилетия накопленных особенностей и зависимостей старого кода трудно. Увы, но таков путь.
На сегодня всё 😉
Привет! После 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🎄
Привет! 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🔥6❤4
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 | Яндекс | браузер
Погружение в механизмы сборки мусора в 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 | Яндекс | браузер
🔥10❤3👍3
Тоже устали от бесконечного снега вокруг и уверены, что зима с нами ещё надолго?
Календарь подсказывает - весна уже через месяц, и в преддверии 8 марта коллеги из Spring АйО и Axiom JDK организуют митап, куда пригласили в том числе и меня - рассказать про Юникод и неочевидные грабли при его обработке. Буду рад видеть лично!
Когда: 05.03.2026, в 18.30
Где: Москва, Лофт Casa Picassa, ул, Бауманская д.11, стр.8. Зал Кандинский
Бесплатно, но нужна регистрация
Подробная программа и регистрация:
https://java-rock-stars.timepad.ru/event/3814065/
Календарь подсказывает - весна уже через месяц, и в преддверии 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; хотим подсчитать их произведение. Посмотрев определение матричного умножения можно сходу придумать что-то такое:
Может показаться неочевидным, но математический результат работы этой функции не зависит от того, в каком порядке записать все три цикла
То есть, существует шесть разных вариантов реализации данного алгоритма умножения, и они отличаются только порядком использования переменных i, j, k в циклах
Каждую реализацию можно обозначить в виде строки:
"i → j → k" - это первая функция
Собственно, вопрос: а какая реализация из этих шести вариантов работает эффективнее?
Если есть объяснение почему - пишите в комментариях!
Пост с разбором разумеется будет
Даны две целочисленные матрицы 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
✅ энергетическая эффективность (сколько энергии потратили на выполнение всей задачи)
В рамках этого разбора под эффективностью будем понимать физическое время выполнения программы. Все просто: какая реализация быстрее перемножает матрицы и завершает работу
Выбираем размер матрицы
Во-вторых, надо бы зафиксировать объем задачи, чтобы можно было сравнивать реализации: тут это просто размер матрицы
Простой бенчмарк
Пишем простейший go-бенчмарк и замеряем
Единственный момент, поскольку у нас присутствуют накладные расходы: начальное создание и заполнение матриц числами - это нужно правильно учесть в дальнейшем
Поэтому в бенчмарке есть
Результаты
Замеры делались на Intel i9-12900K
(для чистоты эксперимента энергоэффективные E-ядра отключены), и вот что получилось:
🥇 i → k → j :
🥈 k → i → j :
🥉 j → i → k :
🥉 i → j → k :
❌ j → k → i :
❌ k → j → i :
Почему-то i → k → j работает быстрее всех, фаворит опроса оказался середняком, а худшие варианты почти в 2,5 раза медленнее лучших.
В следующих постах разбираемся, как же так получилось👇
Выбор метрики
Во-первых, как правильно заметили в комментариях, под эффективностью можно понимать разные вещи, например:
В рамках этого разбора под эффективностью будем понимать физическое время выполнения программы. Все просто: какая реализация быстрее перемножает матрицы и завершает работу
Выбираем размер матрицы
Во-вторых, надо бы зафиксировать объем задачи, чтобы можно было сравнивать реализации: тут это просто размер матрицы
n. Для маленьких матриц вся эта возня с индексами очевидно не будет интересной, поэтому пусть n = 1000Простой бенчмарк
Пишем простейший go-бенчмарк и замеряем
Единственный момент, поскольку у нас присутствуют накладные расходы: начальное создание и заполнение матриц числами - это нужно правильно учесть в дальнейшем
Поэтому в бенчмарке есть
NOP/baseline - матрицы не умножаются, но работа по подготовке матриц ведетсяРезультаты
Замеры делались на Intel i9-12900K
(для чистоты эксперимента энергоэффективные E-ядра отключены), и вот что получилось:
784 ms (по опросу: 2) 942 ms (по опросу: 3)1257 ms (по опросу: 5-6)1302 ms (по опросу: 1)1996 ms (по опросу: 5-6)2044 ms (по опросу: 4)Почему-то i → k → j работает быстрее всех, фаворит опроса оказался середняком, а худшие варианты почти в 2,5 раза медленнее лучших.
В следующих постах разбираемся, как же так получилось👇
Please open Telegram to view this post
VIEW IN TELEGRAM
❤9
Разбор квиза 2/3: гипотеза о кэшах
Паттерны обращения к памяти
Заметим - обращение всегда идет по конкретным индексам:
🌀 матрица А:
🌀 матрица B:
🌀 матрица C:
Формула:
Давайте визуализируем паттерн этих обращений внутри самого вложенного цикла с помощью серого цвета
Например, для i → j → k последний вложенный цикл - по k, а значит в этом цикле:
💡 чтение
💡 чтение
💡 позиция элемента
Кэши процессора
Если бы программа работала на абстрактной RA-машине (на чьей модели и строится теория анализа сложности алгоритмов) - паттерны доступа к памяти не имели бы никакого значения
Однако запускается то программа на реальном железе - в моем случае на x86. Между вычислительными устройствами процессора (они и исполняют инструкции) и памятью (в которой хранятся данные) находится иерархия из нескольких промежуточных кэшей: L1, L2, L3
Причина существования кэшей состоит в том, что обычная память гораздо медленнее современных CPU. Эти кэши очень сложно устроены, и в рамках этого поста не будем сильно погружаться в их устройство. Достаточно рассмотреть только один аспект
Кэш-линии и их вытеснение
Когда происходит чтение области памяти по адресу
Кэш оперирует не отдельными адресами, а непрерывными областями памяти (обычно размером 64 байта, выравнивание такое же)
Когда происходит cache miss - в кэш загружается не только переменная по
Кэш имеет ограниченный размер, поэтому каждая загрузка кэш-линии будет приводить к тому, что какая-то другая кэш-линия будет вытеснена из кэша
Гипотеза
Теперь можно сформулировать теорию
Когда происходят доступы к элементам матрицы в рамках одной строки - загрузка кэш-линии помогает, чтения идут из этой же кэш-линии - это быстро🔋
Когда доступы идут в рамках конкретного столбца - кэш-линия от текущего чтения никак будет использоваться при чтении следующего элемента столбца
Более того, чтение может вытеснять какую-то другую кэш-линию! Таким образом, мы гораздо хуже утилизируем кэш и работаем менее эффективно🔋
Это объясняет почему i → k → j работает эффективнее: обращения идут построчно, что дает лучшую кэш-локальность
Но, конечно же, пока все это только теория. В следующем посте замеряем эффекты на практике 👇
Паттерны обращения к памяти
Заметим - обращение всегда идет по конкретным индексам:
a[i][k]b[k][j]c[i][j]Формула:
c[i][j] += a[i][k] * b[k][j]Давайте визуализируем паттерн этих обращений внутри самого вложенного цикла с помощью серого цвета
Например, для i → j → k последний вложенный цикл - по k, а значит в этом цикле:
a[i][k] будет происходить в рамках строки ib[k][j] будет происходить в рамках столбца jc[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
🔥6❤1
Разбор квиза 3/3: проверка теории
В прошлой части мы сформулировали гипотезу:
порядок циклов влияет на локальность доступа к памяти → локальность влияет на кэш → кэш влияет на время выполнения
Теперь проверим это в реальности
Perf
Используем perf - стандартный Linux инструмент для анализа производительности. Для начала соберем существующий go-бенчмарк в виде бинарного файла:
Для каждого варианта умножения запустим perf в режиме сбора событий:
Что замеряем
В результате для каждого варианта получим результаты в таком формате:
Где:
Из этих двух чисел выводится
Из этих двух чисел выводится, какая доля обращений к L1 оказалась cache-miss
Что получилось
Для каждой реализации я собрал соответствующие события, отнял
Для всех реализаций:
😀 количество инструкций почти одинаковое
😀 разница - в cycles и IPC
😀 разница коррелирует с L1 miss rate
У i → k → j:
✅ меньше всего L1-промахов
✅ выше IPC
✅ меньше cycles
У k → j → i:
❌ больше всего промахов
❌ ниже IPC
❌ больше cycles
Теория полностью совпала с практикой!
Важное уточнение
В прошлом посте мы рассматривали только самый внутренний цикл. Но тот же принцип работает и для промежуточного цикла. Именно поэтому, например, i → k → j и k → i → j тоже отличаются по результатам. Надо учитывать общий паттерн доступа к памяти!
Давайте подводить итоги👇
В прошлой части мы сформулировали гипотезу:
порядок циклов влияет на локальность доступа к памяти → локальность влияет на кэш → кэш влияет на время выполнения
Теперь проверим это в реальности
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 - сколько инструкций реально выполнил CPUcycles - cколько тактов процессора потребовалосьИз этих двух чисел выводится
IPC (instruction-per-cycle) - количественная метрика, показывающая насколько эффективно наша программа работает с точки зрения CPUL1-dcache-loads - количество попыток прочитать данные из L1-кэшаL1-dcache-load-misses - количество случаев, когда запрошенных данных не оказалось в L1 (пошли дальше по пути L2 → L3 → RAM)Из этих двух чисел выводится, какая доля обращений к L1 оказалась cache-miss
Что получилось
Для каждой реализации я собрал соответствующие события, отнял
baseline и отобразил на изображенииДля всех реализаций:
У i → k → j:
У k → j → i:
Теория полностью совпала с практикой!
Важное уточнение
В прошлом посте мы рассматривали только самый внутренний цикл. Но тот же принцип работает и для промежуточного цикла. Именно поэтому, например, 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 - когда есть понимание, как система работает внутри, и не заставляешь ее делать то, что ей неудобно
Понравился ли вам данный лонгрид, делать ли еще такие, или что-то поменять в формате? Пишите в комментариях❤️
Разумеется, никто в реальной жизни таким наивным алгоритмом матрицы не перемножает - есть куда более продвинутые техники. Если интересно, отличный разбор: Achieving Peak Performance for Matrix Multiplication in C++
Но цель этого упражнения была другой - показать, как одинаковый алгоритм с одинаковым количеством инструкций и одинаковой асимптотикой может давать разницу почти в 2,5 раза
Причина - кэши процессора и паттерны доступа к памяти
Ничего не меняя математически, просто начав ходить по памяти так, как это удобно железу - и все стало работать эффективнее
И это, по сути, и есть та самая mechanical sympathy - когда есть понимание, как система работает внутри, и не заставляешь ее делать то, что ей неудобно
Понравился ли вам данный лонгрид, делать ли еще такие, или что-то поменять в формате? Пишите в комментариях
Please open Telegram to view this post
VIEW IN TELEGRAM
YouTube
Achieving Peak Performance for Matrix Multiplication in C++ - Aliaksei Sala - C++Now 2025
https://www.cppnow.org
---
Achieving Peak Performance for Matrix Multiplication in C++ - Aliaksei Sala - C++Now 2025
---
Matrix multiplication is a fundamental operation in scientific computing, game development, AI, and numerous high-performance applications.…
---
Achieving Peak Performance for Matrix Multiplication in C++ - Aliaksei Sala - C++Now 2025
---
Matrix multiplication is a fundamental operation in scientific computing, game development, AI, and numerous high-performance applications.…
🔥20❤2
