Пока идёт горячее обсуждение быть или нет быть 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)
wizzard0
11.05.2017 15:54+5Тут есть ложка дёгтя.
Если прочитать соответствующий коммит, то на Windows они вызывают GetSystemTimeAsFileTime, а надо GetSystemTimePreciseAsFileTime.
Первый метод как раз обращается к старому таймеру (у которого точность от 1 мс до 16 мс), а второй — наносекундный.vladimir_dolzhenko
11.05.2017 16:11+2Я не большой специалист в таких деталях (как-то не рассматриваю windows как адекватную платформу)
Не пробовали занести патч? Написать автору коммита например?
vladimir_dolzhenko
12.05.2017 08:37+2Нашёл схожий баг в .NET #5061 — Use GetSystemTimePreciseAsFileTime for DateTime.UtcNow
Спасибо! Надо занести будет и в жавку.
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 тут просто напрашивается быть номером один в литературном обзоре :-)vladimir_dolzhenko
15.05.2017 23:17+1Спасибо большой за отзыв — дополнил статью — более того, стала очевидна проблема с windows.
lany
16.05.2017 05:33Заметь, кстати, что аллокация гигабайта каждые три секунды замедлила программу всего на 2.5% (что вообще не превышает погрешность) по сравнению с программой без аллокаций. Этот комментарий для тех, кто кричит, что garbage collector'ы тормозят безбожно :-)
tmk826
Грамотно изложено, спасибо.