У жніўні я памераў, колькі каштуе жывы папярэдні прагляд, калі будаваць яго на адкаце транзакцыі. Харнэс я выклаў на 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”. Гэта значыць, што ў межах розніцы ў 2 разы харнэс не адрозніў бы іх. Тая ж фраза дадавала, што не адрозніў бы іх і адзін прагон у кагосьці іншага.
Фраза мне падабалася, бо гучала сціпла. Я яе ні разу не правяраў. Гэта сцверджанне пра тое, што харнэс здольны ўбачыць. Такое можна памераць, калі даць харнэсу эфект, памер якога я ведаю загадзя.
Затрымка вядомага памеру
У кожнай пары адно і тое ж прэв’ю з адкатам з рэпазіторыя выконваецца двойчы. Другому запуску зверху дададзена актыўнае чаканне, 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 разоў асцярожнейшая, чым трэба: розніца ў 2 разы супраць 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 мкс.