Пока идёт горячее обсуждение быть или нет быть 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 библиотеки и не забывать её деплоить.

Поделиться с друзьями
-->

Комментарии (8)


  1. tmk826
    10.05.2017 23:53
    +1

    Грамотно изложено, спасибо.


  1. wizzard0
    11.05.2017 15:54
    +5

    Тут есть ложка дёгтя.

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

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


    1. vladimir_dolzhenko
      11.05.2017 16:11
      +2

      Я не большой специалист в таких деталях (как-то не рассматриваю windows как адекватную платформу)

      Не пробовали занести патч? Написать автору коммита например?


    1. vladimir_dolzhenko
      12.05.2017 08:37
      +2

      Нашёл схожий баг в .NET #5061 — Use GetSystemTimePreciseAsFileTime for DateTime.UtcNow

      Спасибо! Надо занести будет и в жавку.


    1. vladimir_dolzhenko
      17.05.2017 09:48

      Об этой проблеме занёс багу JDK-8180466.


  1. lany
    13.05.2017 12:22
    +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. vladimir_dolzhenko
      15.05.2017 23:17
      +1

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


      1. lany
        16.05.2017 05:33

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