Тёма очень хорошо написал о измерении времени в java с ссылками на источники, так, что казалось бы и добавить нечего, но вставлю я свои пять копеек.
Все мы хорошо знаем метод System.nanoTime()
/**
* Returns the current value of the most precise available system
* timer, in nanoseconds.
*
* This method can only be used to measure elapsed time and is
* not related to any other notion of system or wall-clock time.
* The value returned represents nanoseconds since some fixed but
* arbitrary time (perhaps in the future, so values may be
* negative). This method provides nanosecond precision, but not
* necessarily nanosecond accuracy. No guarantees are made about
* how frequently values change. Differences in successive calls
* that span greater than approximately 292 years (263
* nanoseconds) will not accurately compute elapsed time due to
* numerical overflow.
*
* For example, to measure how long some code takes to execute:
*
* long startTime = System.nanoTime();
* // ... the code being measured ...
* long estimatedTime = System.nanoTime() - startTime;
*
*
* @return The current value of the system timer, in nanoseconds.
* @since 1.5
*/
public static native long nanoTime();
И не смотря на то, что его абсолютные значения слабо связаны с какой-либо начальной точкой во времени, возникают соблазны (чего уж там, и я грешен) сделать гибрид из nanoTime() и currentTimeMillis() - используя последний получить некоторый offset относительно начала эпохи, а nanos в серии измений, чтобы получать разрешение хотя бы микросекундной точности.
Всё бы ничего, если бы не очень важное замечание в javadoc метода nanoTime() - данные значения никак не связаны с системными или настенными часами.
Напрашивается вопрос - с чем же тогда значение связано и как его правильно использовать.
Ответ, вполне очевидный многим - значения берутся из некоторого внутреннего счётчика, это вполне может быть uptime в gnu/linux, или что-нибудь ещё, что взбрело в голову.
Что же касается рамок корректности значений - дабы удешевить данную операцию, нужно использовать некоторые внутрипроцессорные, а ещё лучше внутриядерные счётчики, например, количество тиков с момента запуска процессора.
На x86 архитектуре это выполняется инструкцией Read Time Stamp Counter, которая имеет обширный ряд проблем.
Т.о. последовательные вызовы nanoTime() корректны не в пределах одной jvm, сколько в пределах одного ядра cpu внутри одной jvm.
Небольшой эксперимент по изменению разницы nanoTime() на 50 нитках наглядно демонстрирует разброс:
T1 - T0 : -603.036 ms T2 - T1 : 3.034 ms T3 - T2 : 598.562 ms T4 - T3 : -600.522 ms T5 - T4 : 612.359 ms T6 - T5 : -610.809 ms T7 - T6 : 610.947 ms T8 - T7 : -610.722 ms T9 - T8 : 601.671 ms T10 - T9 : -464.732 ms T11 - T10 : 456.234 ms T12 - T11 : -458.359 ms T13 - T12 : 455.293 ms T14 - T13 : -456.188 ms T15 - T14 : 456.052 ms T16 - T15 : 5.646 ms T17 - T16 : -353.449 ms T18 - T17 : 353.424 ms T19 - T18 : -457.635 ms T20 - T19 : 462.279 ms T21 - T20 : -460.684 ms T22 - T21 : 457.1 ms T23 - T22 : -459.267 ms T24 - T23 : 453.619 ms T25 - T24 : 5.864 ms T26 - T25 : -3.459 ms T27 - T26 : 4.51 ms T28 - T27 : -1.376 ms T29 - T28 : 0.453 ms T30 - T29 : 1.867 ms T31 - T30 : -9.318 ms T32 - T31 : -452.341 ms T33 - T32 : 460.413 ms T34 - T33 : 12.483 ms T35 - T34 : -14.287 ms T36 - T35 : 14.254 ms T37 - T36 : 0.758 ms T38 - T37 : -10.808 ms T39 - T38 : 0.285 ms T40 - T39 : -5.638 ms T41 - T40 : -1.892 ms T42 - T41 : 17.851 ms T43 - T42 : -19.797 ms T44 - T43 : 19.406 ms T45 - T44 : -12.41 ms T46 - T45 : 2.643 ms T47 - T46 : -5.896 ms T48 - T47 : 4.104 ms T49 - T48 : -599.48 ms
Т.о. гидра из nanoTime() и currentTimeMillis() будучи использована в разных нитках способна наверняка преподнести сюрпризы.
Счастье не наступило, и у нас нет в руках инструмента, чтобы замерять время в java с микросекундной точностью.
Keep calm and carry on.
Заглянув под капот System.nanoTime(), кому лень - те смотрят в исходники openjdk, например, openjdk/hotspot/src/os/linux/vm/os_linux.cppjlong os::javaTimeNanos() {
if (Linux::supports_monotonic_clock()) {
struct timespec tp;
int status = Linux::clock_gettime(CLOCK_MONOTONIC, &tp);
assert(status == 0, "gettime error");
jlong result = jlong(tp.tv_sec) * (1000 * 1000 * 1000) + jlong(tp.tv_nsec);
return result;
} else {
timeval time;
int status = gettimeofday(&time, NULL);
assert(status != -1, "linux error");
jlong usecs = jlong(time.tv_sec) * (1000 * 1000) + jlong(time.tv_usec);
return 1000 * usecs;
}
}
и System.currentTimeMillis():
jlong os::javaTimeMillis() {
timeval time;
int status = gettimeofday(&time, NULL);
assert(status != -1, "linux error");
return jlong(time.tv_sec) * 1000 + jlong(time.tv_usec / 1000);
}
Разбираемся с
int clock_gettime(clockid_t clk_id, struct timespec *tp);
где clk_id идентификатор используемых часов, а timespec - структура секунда - наносекунда:struct timespec {
time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
Вся соль в clk_id:
CLOCK_REALTIME
Системные «настенные» часы, предоставляющие время с начала эпохи.CLOCK_MONOTONIC
Монотонные часы относительно некоторого не специфицированного момента времени. Именно CLOCK_MONOTONIC использует инструкцию RDTSC.(и ещё пара не столь важных для нас значений типа CLOCK_PROCESS_CPUTIME_ID и CLOCK_THREAD_CPUTIME_ID.)
И вот мы почти на финише, объявляем
public final class PreciseTimestamp {
static {
try {
System.loadLibarary("precisetimestamp");
} catch (Throwable e){
// bla-bla-bla
}
}
public static native long getMicros();
}
В long можно уложить разницы в 292471 лет с точностью до микросекунды, на первое время должно хватить.
javah создаёт из java класса c/c++ заголовок, *nix реализация:
#include <stdlib.h>
#include <sys/time.h>
#include <time.h>
#include <unistd.h>
#include "PreciseTimestamp.h"
JNIEXPORT jlong JNICALL Java_PreciseTimestamp_getMicros
(JNIEnv * env, jclass jc)
{
#if defined(_POSIX_TIMERS)
{
struct timespec ts;
if ( 0 == clock_gettime(CLOCK_REALTIME, &ts) )
{
return ((((jlong) ts.tv_sec) * 1000000) +
(((jlong) ts.tv_nsec)) / 1000);
}
}
#endif
// otherwise use gettimeofday
struct timeval tv;
gettimeofday(&tv, NULL);
return (((jlong) tv.tv_sec) * 1000000) + ((jlong) tv.tv_usec);
}
И с силой
Поднимая разговор о ntp нельзя не отметить, что он вносит изменения в значения CLOCK_REALTIME, т.е. в моменты синхронизации можем получать странные значения, в т.ч. и отрицательные.
Тонкие ценители прекрасного могли бы, конечно, воспользоваться не jni, а jna, но не стоит пренебрегать рекомендацией авторов jna - каждый вызов тяжелее ~ 0.1 мс, что не пригодно для поставленной цели.
5 комментариев:
Прикольно! микросекундный таймер - своими руками. Клево. Интересно, что уже столько лет платформе Java, а некоторых практических вещей вроде этого таймера - нет. А с другой стороны, есть столько всего другого, что хоть отбавляй. : )
По поводу упомянутой "хорошо написанной статьи" хотел сказать - с фактической стороны тема раскрыта, интересно и информативно, но русский язык жалко - и даже не из-за англицизмов (впрочем, с ними тоже полегче надо), а на более базовом уровне:
"Таким образом, если вы заметили, что ваши таски скедуляться с сильным джиттером на вашей платформы, то можете заменить одну имплементацию на другую, и возможно положение улучшиться [1]."
@ST:
LJC (London Java Community) среди прочего обсуждает, что неплохо бы какие-то низкоуровневые вещи, такие например как флаг переполнения целого и многие другие, поднять на уровень, если хоть не java, то хотя бы jvm.
Ещё один интересный аспект - intrinsic - возможно Руслан или я в ближайшее время что-нибудь да напишем - словом с точки зрения архитектуры jvm это действительно чёрная магия. Буквально на днях Peter Lawrey сравнивал Integer.bitCount и такой же самый с точки зрения java кода, но вставленный не в java.lang.Integer, а в свой пакет. Разница - почти в 6 раз, но не в пользу последнего.
Т.о., то, что мы видим java код - это все лишь фасад и как оно на самом деле работает и через что - это порой ещё тот вопрос.
Очень верное замечание по поводу языка, стараюсь и в статьях, и в тех немногочисленных докладах, что у меня были, употреблять русские выражения и, конечно же, читать побольше художественной литературы, поднимая свой уровень.
Слава, к сожалению, по русскому языку я всегда с тройки на четверку перебивался. Это, наверное, был главный консенр (я - специально :) ) во время принятия решении о начале блога. Но я поборол стеснения и все же решил попробовать. Получилось, конечно, не ахти, но уж как получилось. По поводу англицизмов, я сразу решил, что не буду стесняться в их использовании, так как либо их толком и не перевести, либо очень долго нужно изворачиваться, чтобы найти более менее нормальный аналог. При этом все равно найдуться недовольные, кто в комменты напишут, что не правильно перевел, какое бы слово я не подобрал :)
Володь, в дойче, когда я интегрировался с одной библиотечкой, написанной внутри банка, там как раз был нативный метод, дающий микросекундную точность, чтобы мерить letancy с учетом прохода ордера через несколько боксов. Можно из интереса заглянуть в ее реализацию, может там еще что-то умное люди придумали. Если решишь посмотреть, то напиши мне пиьсмо - я тебе в личку скину, где это поискать.
@Artem:
если я правильно понимаю, ты говоришь о jninanos, написанной Matt'ом - в общем-то код приведённый тут очень близок к ней, что впрочем и не удивительно - методы ведь те же самые, что и везде (в том же nanoTime, currentTimeMillis)
Ага, про него. Спасибо.
Отправить комментарий