В августе я измерил, сколько стоит живое превью, если строить его на откате транзакции. Харнесс я выложил на GitHub как preview-cost. На 4500 парах откат каждый раз проигрывал копии в памяти, медианная разница вышла от 6 до 7 мс. В README я написал ещё и абзац про шум. На 40 планах медианы 5 отдельных JVM лежали от 5,43 до 7,79 мс на одинаковых входах. Отсюда я вывел, что если бы 2 стратегии отличались “within 2× of each other, this harness could not have told them apart”. То есть в пределах двукратной разницы харнесс бы их не различил. Та же фраза добавляла, что не различил бы их и ничей другой одиночный прогон.
Фраза мне нравилась, потому что звучала скромно. Я её так и не проверил. Это утверждение о том, что харнесс способен увидеть. Такое можно измерить, если дать харнессу эффект, размер которого я знаю заранее.
Задержка известного размера
В каждой паре одно и то же превью с откатом из репозитория выполняется дважды. Ко второму запуску добавлено активное ожидание, Thread.onSpinWait() до тех пор, пока System.nanoTime() не пройдёт заданный срок. Значит он стоит на d микросекунд дороже, плюс несколько десятков наносекунд. Я перебрал 8 значений d от 0 до 500 мкс. Для каждого запускалось 10 свежих JVM, в каждой 300 пар после 80 пар разогрева. Внутри JVM превью без задержки и превью с задержкой меняются местами случайно в каждой паре, как в исходном Bench.
На этот раз замер шёл на PostgreSQL 16.14, установленном локально, а не в Docker, на OpenJDK 21.0.12.1 в виртуальной машине с 4 vCPU. Репозиторий просит у Gradle JDK 25, но нужные мне классы не используют ничего новее records и text blocks, поэтому я собрал их обычным javac вместе с новым раннером. В этой конфигурации превью с откатом стоит от 0,7 до 1,1 мс вместо 6,5. Исходное сравнение остаётся в силе: откат был медленнее в 2998 парах из 3000.
Разброс тоже вернулся. При d = 0 оба превью в паре одинаковые и медианы 10 JVM лежали от 746 до 1005 мкс. Это треть величины для одной и той же команды на одной и той же машине. У копии в памяти в исходном сравнении одна JVM дала 278 мкс, а у остальных 9 было от 77 до 102.
Что видит одна JVM
Для каждой JVM я взял разности внутри пар и интервал бутстрепа для их медианы, 2000 перевыборок. Задержку я считаю увиденной, когда весь интервал лежит выше нуля.
| истинная задержка | JVM, которые её увидели | все 3000 пар, медиана и 95% интервал |
|---|---|---|
| 0 мкс | 0 из 10 | 0,6 мкс [−3,1; 3,7] |
| 5 мкс | 3 из 10 | 8,1 мкс [5,1; 11,1] |
| 10 мкс | 6 из 10 | 12,2 мкс [8,1; 15,3] |
| 25 мкс | 10 из 10 | 25,0 мкс [21,5; 28,7] |
25 мкс поверх примерно 800 это 3% и все 10 JVM их нашли. Медианы превью без задержки в тех же JVM расходились примерно на 210 мкс. Ни при одной задержке ни у одной JVM весь интервал не оказался ниже нуля, включая прогон A/A. Значит для этого харнесса моя фраза была осторожнее нужного примерно в 30 раз, двукратная разница против 3%.
Объясняет это парность. От JVM к JVM медиана превью без задержки и медиана превью с задержкой меняются вместе. Корреляция от 0,95 до 0,996, в зависимости от задержки. Моя догадка такая: то, что делает одну JVM медленнее, оплачивают обе половины пары, поэтому в разности почти всё это сокращается.
Где фраза верна
Верна её вторая половина и она описывает другую процедуру. Эту процедуру я и сам использую чаще всего: запустил, что-то поменял, запустил ещё раз, сравнил 2 медианы. Я смоделировал её так: медиана без задержки из одной JVM, медиана с задержкой из другой. 10 JVM дают 90 таких сочетаний.
На 100 мкс, это 12% величины, знак верно определили 68 сравнений из 90. Их оценки были от −157 до +385 мкс. На 50 мкс вышло 62 из 90, а совсем без задержки 47 из 90 сказали, что второе превью медленнее. Только на 250 мкс, что близко к разбросу между JVM, все 90 показали в правильную сторону.
Первая версия
Первая версия харнесса мерила сначала все превью с откатом, потом все превью в памяти, внутри одной JVM. Она ушла гистом 14 августа с одной медианой на конфигурацию: 4,85; 4,96 и 7,79 мс. Меньше чем через 2 часа я заменил её парной, потому что знал, что порядок блоками неверен в принципе. Насколько неверен, я не знал, поэтому прогнал и такой порядок. Мой раннер оставляет разогрев новой версии, так что он повторяет порядок старой, но не её целиком.
В случае A/A второй блок вышел быстрее первого в 8 JVM из 10, на величину от 119 до 173 мкс. В остальных 2 он был медленнее на 10 и 16. С истинной задержкой 50 мкс на втором блоке 7 JVM из 10 назвали его более быстрым. На 100 мкс таких было 4 из 10. Отделить порядок от времени, которое JVM уже успела проработать, я не могу: в этой схеме это одно и то же. Рядом с августовскими 6 мс смещение такого размера ничего бы не поменяло, но в той конфигурации я его не мерил.
Чего я не проверял
Я не знаю, почему JVM расходятся на треть. Я не смотрел логи JIT и не смотрел, что ещё крутится на этой виртуальной машине. Есть и мелочь, которую я не могу объяснить. На 250 и 500 мкс оценка по всем парам вместе 244,7 и 488,5 и ни один интервал не доходит до истинного значения. Активное ожидание не может закончиться раньше срока, значит часть разности теряется где-то ещё. Мой раннер не записывает, какое превью шло первым, поэтому сказать, где именно, я не могу. На 5 мкс оценка 8,1. Это выше истинного значения и это я тоже объяснить не могу. Все числа получены одним проходом сетки, а не медианой нескольких проходов.
Как метод всё это не ново. Georges, Buytaert и Eeckhout в 2007 году предлагали интервалы по нескольким запускам VM. Kalibera и Jones в 2013 году вывели, сколько повторов нужно на каждом уровне. А проверил я только свою фразу на своём коде. Если бы я писал эту строку README сегодня, она была бы про 2 отдельных прогона. В этой конфигурации они не различили бы варианты, которые отличаются меньше чем примерно на 30%. Внутри одной JVM хватило 25 мкс.