perf(reader): CI skip-list кэши → get-first ConcurrentHashMap#644
Conversation
…t ConcurrentHashMap Профилирование чтения показало, что кэши TransformationUtils (METHODS/TYPES/SETTERS) на ConcurrentSkipListMap с компаратором CASE_INSENSITIVE_ORDER — крупнейший прикладной hotspot чтения: ~6% CPU уходит на ConcurrentSkipListMap.doGet + StringLatin1.compareToCI при поиске метода/типа по имени XML-узла. Регистронезависимость нужна лишь при первом резолве (скан equalsIgnoreCase по методам класса), а не на каждый поиск в кэше. Переводим кэши на ConcurrentHashMap с точным ключом-именем узла (имена узлов повторяются и интернированы Xerces) — поиск O(1) по хешу без CI-сравнений. Первый резолв по-прежнему матчит имя регистронезависимо. Доступ к кэшу — get()-first, а не computeIfAbsent с захватывающей лямбдой: ConcurrentHashMap.computeIfAbsent не встраивается JIT-ом, из-за чего лямбда-функция отображения перестаёт скаляризоваться escape-анализом и аллоцируется на каждый вызов. На попадании в кэш (горячий путь) лямбда не создаётся. Замеры (JMH MDClassesBenchmark на cpm, JFR sample share — устойчив к шуму машины): - доля CI-поиска в CPU чтения: 6.0% -> 1.1% (-4.9 п.п.); - аллокации: без изменений (5078 -> 5077 МБ/чтение), лямбды кэша не аллоцируются; - wall: 9065 -> 8887 мс (в пределах шума). Поведение не меняется. Тесты TransformationUtilsTest и интеграционные MDOReaderTest зелёные. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HLvkKQCwBjTHwuZ4EGBQK6
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
get()-first проверял значение кэша (Optional) на null — Sonar S2789 (Optional не должен быть null). Убираем Optional из значений кэшей METHODS/TYPES/SETTERS: храним сырые Method/Type/SetterDescriptor, а отрицательный результат — через сентинелы (в ConcurrentHashMap нельзя хранить null). Перф opt-A сохранён: CI-поиск ~1% CPU, аллокации 5078 МБ/чтение (без изменений), лямбды кэша не аллоцируются. Заодно упрощён perClassCache: лямбда k -> new ConcurrentHashMap<>() незахватывающая (синглтон), поэтому computeIfAbsent там безопасен по аллокациям — get()-first обёртка не нужна. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HLvkKQCwBjTHwuZ4EGBQK6
|



Описание
Профилирование чтения (JFR на cpm) показало, что кэши
TransformationUtils(
METHODS/TYPES/SETTERS) наConcurrentSkipListMapcCASE_INSENSITIVE_ORDER—крупнейший прикладной hotspot чтения: ~6 % CPU на
ConcurrentSkipListMap.doGet+StringLatin1.compareToCIпри поиске метода/типа по имени XML-узла.Регистронезависимость нужна только при первом резолве (скан
equalsIgnoreCaseпо методамкласса), а не на каждый поиск. Переводим кэши на
ConcurrentHashMapс точным ключом-именемузла (имена узлов повторяются и интернированы Xerces) — поиск O(1) по хешу без CI-сравнений.
Доступ к кэшу —
get()-first, а неcomputeIfAbsentс захватывающей лямбдой:ConcurrentHashMap.computeIfAbsentне встраивается JIT-ом, из-за чего лямбда отображенияперестаёт скаляризоваться escape-анализом и аллоцируется на каждый вызов (профиль поймал
405 сэмплов аллокации лямбд, +170 МБ/чтение у наивного варианта).
get()-first убирает это.Замеры (JMH
MDClassesBenchmarkна cpm; CPU — доля JFR-сэмплов, устойчива к шуму машины)CPU-выигрыш реальный и измерен через долю сэмплов (не зависит от разброса wall). Wall — в
пределах шума машины (чтение параллельное, GC 5 ГБ/op).
Связанные задачи
Closes
Чеклист
Общие
TransformationUtilsTest; интеграционныеMDOReaderTestзелёные)gradlew precommit) — прогонял reader-тестыДополнительно
Регресс аллокации лямбд у наивного
ConcurrentHashMap.computeIfAbsentпойман профилированием —в PR только исправленный
get()-first вариант.