Precise timestamp

    Пока идёт горячее обсуждение быть или нет быть jigsaw в java 9 и в каком виде ему быть — не стоит забывать про полезняшки, которые несёт с собой девятка — и одна из них — повышение точности Clock.systemUTC()JDK-8068730.


    Что же было раньше ?


    До java 8 был System.currentTimeMillis() и System.nanoTime(), и если первый давал wall clock время, но с миллисекундным разрешением, то второй даёт время с разрешением до наносекунд, но область применения ограничена измерением разности времён, причём в рамках одной jvm — и ни о каком использовании такой временной метки между разными машинами и быть не может.


    Поэтому часто велосипедят свои precise timestamp дающие wall clock время с большим разрешением, чем у currentTimeMillis (используя jni со всеми вытекающими) — более подробно про разницу между currentTimeMillis и nanoTime, и про велосипед можно почитать в моём старом посте.


    Java 8 заложил очень мощный фундамент — Java Time API. С ним можно сказать пока и joda time, и встроить свой велосипед в java.time.Clock, т.к. штатный SystemClock по своей сути работает поверх System.currentTimeMillis() и не может обеспечить разрешение, лучше, чем миллисекунда.


    И вот теперь в игру вступает java 9 и ничего не ломая (что касается времени и его измерения) приносит улучшение — можно выбросить свой jni велосипед, по крайней мере на Linux, MacOSX, BSD, Solaris и Windows — см. коммит в openjdk.


    С практической точки зрения имеют смысл микросекундные временные метки, а не наносекундные — причина тому, что ntp в рамках intranet способна дать время с точностью до 1 мкс.


    Запилим провайдера точного wall clock времени с микросекундным разрешением (исходники в т.ч. и native часть):


    public final class PreciseTimestamp {
        static final Clock clock = Clock.systemUTC();
    
        static {
            try {
                System.loadLibrary("precisetimestamp");
            } catch (Throwable e){
               throw new RuntimeException(e.getMessage(), e);
            }
        }
    
        // JNI microsecond timestamp provider
        public static native long getMicros();
    
        // Critical JNI microsecond timestamp provider
        public static native long getCMicros();
    
        public static long clockMicros(){
            final Instant instant = clock.instant();
            return instant.getEpochSecond() * 1_000_000L + (instant.getNano() / 1_000);
        }
    }    

    Java 8 даст примерно такие результаты


    JNI micros:             1494398000506568
    Critical JNI micros:    1494398000506986
    ClockUTC micros:        1494398000507000
    currentTimeMillis:      1494398000507

    Ничего удивительного — внутри старый-добрый System.currentTimeMillis()


    И Java 9:


    JNI micros:             1494398039238236
    Critical JNI micros:    1494398039238439
    ClockUTC micros:        1494398039238498
    currentTimeMillis:      1494398039239

    Дополнено:
    Т.е. выглядит так, что штатный SystemClock можно использовать как замену jni велосипеду — считаем, что мы можем доверять подлезжащей реализации, что касается корректности получения и монотонности возрастания wall clock времени, рассмотрим гранулярность (тест на гранулярность и результаты ):


    java 9:
    
    OS             Value          Units
    MacOSX:     1011.522          ns/op
    Windows:  999916.218          ns/op
    Linux:      1002.419          ns/op
    

    Т.о. резонность интуитивного предположения об использовании микросекундной точности подтверждается и измерением, за исключением windows, которая обладает известной проблемой с использованием GetSystemTimeAsFileTime — на эту проблему была зарепорчена бага JDK-8180466.


    Но как же перформанс ? — крикнут перформансники — и будут правы — на лицо лишнее создание объекта Instant.


    Пилим benchmark, который сравнивает конечно же jni-велосипед (и обычный, и critical), метод основанный на clock, и для оценки масштабов бедствия System.currentTimeMillis() и System.nanoTime():



    Смотрите доклад про Escape Analysis и скаляризацию, если не понятно, почему вызов clock.instant() оказывается дороже (хоть и не намного), чем вызов более сложного метода clockMicros:


    public static long clockMicros(){
        final Instant instant = clock.instant();
        return instant.getEpochSecond() * 1_000_000L + (instant.getNano() / 1_000);
    }

    Дополнено:
    Убедится в том, что работает скаляризация можно добававив -prof:gc:


    Benchmark                                                           Mode  Cnt    Score    Error   Units
    PerfTiming.clockMicros:·gc.alloc.rate                               avgt    5   ≈ 10⁻⁵           MB/sec
    PerfTiming.clockMicros:·gc.alloc.rate.norm                          avgt    5   ≈ 10⁻⁶             B/op
    
    PerfTiming.instant:·gc.alloc.rate                                   avgt    5  327,083 ± 13,098  MB/sec
    PerfTiming.instant:·gc.alloc.rate.norm                              avgt    5   24,000 ±  0,001    B/op
    

    Выводы: Использование SystemClock в 9ке вполне может заменить jni велосипед — цена ~10% от вызова, много это или мало это — каждый решает сам — я готов жертвовать этими 10%, чтобы забыть головную боль про сборку jni библиотеки и не забывать её деплоить.

    Share post

    Similar posts

    Comments 8

      +1
      Грамотно изложено, спасибо.
        +5
        Тут есть ложка дёгтя.

        Если прочитать соответствующий коммит, то на Windows они вызывают GetSystemTimeAsFileTime, а надо GetSystemTimePreciseAsFileTime.

        Первый метод как раз обращается к старому таймеру (у которого точность от 1 мс до 16 мс), а второй — наносекундный.
        +4

        Плюсанул статью авансом, но у меня к ней много придирок :-)


        Во-первых, измерять такую системно-зависимую штуку явно стоит на разных ОС. И уж как минимум, упомянуть в статье, к какой ОС относятся результаты.


        Во-вторых, вывод, что clock.instant() дороже, чем clockMicros, неверен. На графиках диапазон результатов с учётом рисок погрешности существенно пересекается, однозначно говорить о победе одного варианта над другим нельзя.


        В-третьих, если хочется убедиться, что аллокаций не происходит (то есть escape-анализ работает), надо аллокации и измерять (например, через -prof:gc), а не по наносекундам делать косвенный вывод. Вполне возможно, что аллокация и происходит, просто tlab-аллокация очень быстрая (об этом ты сам говорил на JPoint), и разница не превышает погрешность. А GC тоже может работать очень быстро, так как выживших объектов вообще нет. Scavenger посещает только живые объекты от рутсета, которые попадают в то же поколение (определяется сравнением адреса с границами поколения). Как только все живые объекты (инфраструктура джавы и самого бенчмарка) запромотились на первой сборке, каждая последующая минорная сборка будет выкидывать весь Eden целиком, даже не пробегаясь по нему, а просто проверяя, что ни один объект из рутсета не указывает в Eden. Конечно, если голосовать вслепую, я всё же верю в escape-анализ здесь, но инженер не должен верить, а должен измерять :-)


        Наконец, даже если ClockUTC.micros() выдал 1494398039238498, нельзя сделать вывод, что точность повысилась. Кто знает, может последние цифры просто рандомные или обновляются редко? Здесь полезен бенчмарк на гранулярность, который наш любимый Лёша делал несколько лет назад. Вот для nanotime на Windows у Алексея получаются интересные результаты. Хотя там не нолики на конце, но гранулярность вовсе не 1 нс, а где-то 370 нс. Подобная информация прекрасно дополнила бы эту статью. Вообще, конечно, раз проводишь исследование, стоит ознакомиться с предыдущими работами на эту тему, и Nanotrusting the Nanotime тут просто напрашивается быть номером один в литературном обзоре :-)

          +1
          Спасибо большой за отзыв — дополнил статью — более того, стала очевидна проблема с windows.
            0

            Заметь, кстати, что аллокация гигабайта каждые три секунды замедлила программу всего на 2.5% (что вообще не превышает погрешность) по сравнению с программой без аллокаций. Этот комментарий для тех, кто кричит, что garbage collector'ы тормозят безбожно :-)

        Only users with full accounts can post comments. Log in, please.