14 нояб. 2012 г.

java: nanoTime

Пока одни заливали соляру в трактор, мы продолжаем копаться в своём.

Тёма очень хорошо написал о измерении времени в 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.cpp
jlong 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);
}

И с силой божьейgcc собираем, запускаем и наслаждаемся микросекундным таймером, который можно смело использовать не только в разных нитках, но и разных jvm, и на разных боксах, с учётом того, что они синхронизируются, т.к ntp согласно спецификации может предоставлять микросекундную точность.

Поднимая разговор о ntp нельзя не отметить, что он вносит изменения в значения CLOCK_REALTIME, т.е. в моменты синхронизации можем получать странные значения, в т.ч. и отрицательные.

Тонкие ценители прекрасного могли бы, конечно, воспользоваться не jni, а jna, но не стоит пренебрегать рекомендацией авторов jna - каждый вызов тяжелее ~ 0.1 мс, что не пригодно для поставленной цели.

5 комментариев:

ST комментирует...

Прикольно! микросекундный таймер - своими руками. Клево. Интересно, что уже столько лет платформе Java, а некоторых практических вещей вроде этого таймера - нет. А с другой стороны, есть столько всего другого, что хоть отбавляй. : )

По поводу упомянутой "хорошо написанной статьи" хотел сказать - с фактической стороны тема раскрыта, интересно и информативно, но русский язык жалко - и даже не из-за англицизмов (впрочем, с ними тоже полегче надо), а на более базовом уровне:


"Таким образом, если вы заметили, что ваши таски скедуляться с сильным джиттером на вашей платформы, то можете заменить одну имплементацию на другую, и возможно положение улучшиться [1]."

Vladimir Dolzhenko комментирует...

@ST:

LJC (London Java Community) среди прочего обсуждает, что неплохо бы какие-то низкоуровневые вещи, такие например как флаг переполнения целого и многие другие, поднять на уровень, если хоть не java, то хотя бы jvm.

Ещё один интересный аспект - intrinsic - возможно Руслан или я в ближайшее время что-нибудь да напишем - словом с точки зрения архитектуры jvm это действительно чёрная магия. Буквально на днях Peter Lawrey сравнивал Integer.bitCount и такой же самый с точки зрения java кода, но вставленный не в java.lang.Integer, а в свой пакет. Разница - почти в 6 раз, но не в пользу последнего.

Т.о., то, что мы видим java код - это все лишь фасад и как оно на самом деле работает и через что - это порой ещё тот вопрос.

Очень верное замечание по поводу языка, стараюсь и в статьях, и в тех немногочисленных докладах, что у меня были, употреблять русские выражения и, конечно же, читать побольше художественной литературы, поднимая свой уровень.

Artem Danilov комментирует...

Слава, к сожалению, по русскому языку я всегда с тройки на четверку перебивался. Это, наверное, был главный консенр (я - специально :) ) во время принятия решении о начале блога. Но я поборол стеснения и все же решил попробовать. Получилось, конечно, не ахти, но уж как получилось. По поводу англицизмов, я сразу решил, что не буду стесняться в их использовании, так как либо их толком и не перевести, либо очень долго нужно изворачиваться, чтобы найти более менее нормальный аналог. При этом все равно найдуться недовольные, кто в комменты напишут, что не правильно перевел, какое бы слово я не подобрал :)

Володь, в дойче, когда я интегрировался с одной библиотечкой, написанной внутри банка, там как раз был нативный метод, дающий микросекундную точность, чтобы мерить letancy с учетом прохода ордера через несколько боксов. Можно из интереса заглянуть в ее реализацию, может там еще что-то умное люди придумали. Если решишь посмотреть, то напиши мне пиьсмо - я тебе в личку скину, где это поискать.

Vladimir Dolzhenko комментирует...

@Artem:

если я правильно понимаю, ты говоришь о jninanos, написанной Matt'ом - в общем-то код приведённый тут очень близок к ней, что впрочем и не удивительно - методы ведь те же самые, что и везде (в том же nanoTime, currentTimeMillis)

Artem Danilov комментирует...

Ага, про него. Спасибо.