Pull to refresh

Comments 23

Ого, реально интересная фича! Спасибо за статью!

Я правильно понимаю из кода
+  void enter_critical() { assert(Thread::current() == this ||
+                                 Thread::current()->is_VM_thread() && SafepointSynchronize::is_synchronizing(),
+                                 "this must be current thread or synchronizing");

Что этим ассертом, а именно «Thread::current()->is_VM_thread()», разработчики хотели ограничить использование JavaCritical_ не внутри JVM?
Не, этим только проверяется, что мы не пытаемся изменить состояние чужого потока.
Но любой поток может сам заходить в jni_critical (Thread::current() == this).
Если оно только из горячесго кода, то что будет, если только critical-функцию определить? Ошибка на начальных этапах исполнения программы? А достаточно просто два метода с разными именами и параметрами сделать, чтобы работало?
Если есть только JavaCritical_myMethod, но нет Java_myMethod, бросится UnsatisfiedLinkError. Функция со стандартной сигнатурой нужна всегда. Для удобства она может просто распаковывать аргументы и передавать их уже в JavaCritical_ реализацию.
Вы сами как-то использовали эту возможность?
Сам только недавно наткнулся, копаясь в исходниках Хотспота.
Есть планы опробовать на нашем Java сервере, где всё сетевое I/O на native методах.
А зачем на нативных методах? В Java вроде приличное сетевое API.
— Не поддерживаются TCP опции: TCP_DEFER_ACCEPT, TCP_CORK и т.п.
— Не поддерживаются флаги send и recv: MSG_MORE, MSG_PEEK…
— Selector глючный и не потокобезопасный.
— Нельзя делеать select() на блокирующих сокетах.
— setSoTimeout не работает на SocketChannel'ах.
Да и по производительности можно выжать больше.
Меня больше интересует сколько времени эти вызовы занимают по сравнению со временем работы самого алгоритма. Это как и с накладными расходами на создание потоков. Если потоки долгоживущие/алгоритм долго выполняется, то все эти накладные расходы нивелируются.
и, самое главное, метод должен завершаться за короткое время

Это сколько?
Меня больше интересует сколько времени эти вызовы занимают по сравнению со временем работы самого алгоритма.
Так из графиков же видно. Сравните arrayElementsCritical (стандартный JNI) с javaCritical: делают они одно и то же, значит, вся разница — и есть накладные расходы. Собственно, абзац после графиков отвечает на вопрос.

Это сколько?
Зависит от приложения. Для одних паузы в 100 мс критичны, другие и 5 секунд могут подождать.
Скажем так: если в 10 мс укладывается, значит, для большинства случаев сгодится.
Так из графиков же видно. Сравните arrayElementsCritical (стандартный JNI) с javaCritical: делают они одно и то же, значит, вся разница — и есть накладные расходы. Собственно, абзац после графиков отвечает на вопрос.

В данном вопросе не могу не процитировать Кнута: «Преждевременная оптимизация — корень всех зол». Если процесс выполняется долго, то нет смысла возможно это использовать. А если быстро, то почему бы это и в Java не делать?
Причины есть. Раз уж даже консервативные разработчики JVM это реализовали.
Не всё можно сделать в Java. Например, вы никак не заставите JIT-компилятор использовать SIMD. Но при этом и native не будет выигрывать, поскольку вся выгода уйдет на JNI-обертки.
Скажите пожалуйста, а native методы не попадают под JIT оптимизации? Если попадают, то как происходят эти оптимизации и есть ли вообще в них смысл для native методов?
Для native методов JIT-компилятор генерирует обёртки. Т.е. вся описанная процедура (создание фрейма, перекладывание аргументов, проверка safepoint и т.д.) происходит в динамически скомпилированном коде под данный конкретный метод. Но непосредственно с нативной реализацией метода, которая уже и так в бинарном виде, JIT, естественно, ничего сделать не может и просто вызывает, как есть.
Внезапно возник вопрос, а зачем в JVM появился JavaCritical_, если для этого уже есть интринсики. В чем фундаментальное отличие?
Некорректное утверждение. Вы не можете взять и написать интринсик для «этого».
Совсем разные вещи.
Интринсики — часть JVM, их пишут компиляторщики. По сути, это переписывание тела метода на суровом хотспотовском IR.
JavaCritical — оптимизация для библиотек. Чтобы код на C, написанный не JVM разработчиками, эффективней вызывался.
Посмотрите, как выглядит хоть один интринсик, например, LibraryCallKit::string_indexOf, и всё станет сразу ясно.
Хм, спасибо. После прочтения поста в голове отложилось, что JavaCritical_ — это внутренний API для самих же разработчиков JVM, поэтому и возник вопрос.
Когда-то давным-давно была потребность измерять время с мкс разрешением — т.к. штатно в java это нельзя сделать — мотивация простая — хочется мерять время между боксами — и готовы терпеть jni вызовы и погрешность, но на боксах ntp работает достаточно точно.

И вот решил таки попробовать напилить то же самое, но с Critical JNI:

package com;

public final class PreciseTimestamp {

    static {
        try {
            System.loadLibrary("precisetimestamp");
        } catch (Throwable e){
           throw new RuntimeException(e.getMessage(), e);
        }
    }

    public static native long getMicros();

    public static native long getCMicros();

    public static void main(String[] args) {
        System.out.println(PreciseTimestamp.getMicros());
        System.out.println(PreciseTimestamp.getCMicros());
        System.out.println(System.currentTimeMillis());
    }

}


к нему com_PreciseTimestamp.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_PreciseTimestamp */

#ifndef _Included_com_PreciseTimestamp
#define _Included_com_PreciseTimestamp
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_PreciseTimestamp
 * Method:    getMicros
 * Signature: ()J
 */
JNIEXPORT jlong JNICALL Java_com_PreciseTimestamp_getMicros
  (JNIEnv *, jclass);

/*
 * Class:     com_PreciseTimestamp
 * Method:    getCMicros
 * Signature: ()J
 */
JNIEXPORT jlong JNICALL Java_com_PreciseTimestamp_getCMicros
  (JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif


и com_PreciseTimestamp.c
#include <stdlib.h>
#include <sys/time.h>
#include <time.h>
#include <unistd.h>
#include "com_PreciseTimestamp.h"

JNIEXPORT jlong JNICALL Java_com_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);
}


JNIEXPORT jlong JNICALL Java_com_PreciseTimestamp_getCMicros
  (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);
}

JNIEXPORT jlong JNICALL JavaCritical_com_PreciseTimestamp_getCMicros
  ()
{
//#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);
}


(поскольку проверка гонялась не на linux, а на macosx)

и конечно же benchmark

package com;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

/**
 * @author vladimir.dolzhenko@gmail.com
 * @since 2016-11-17
 */
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Fork(1)
@Warmup(iterations = 3, time = 10000, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 3, time = 10000, timeUnit = TimeUnit.MILLISECONDS)
@Threads(1)
public class PerfTiming {
	@Benchmark
	public void testSystemNano(Blackhole bh){
		long v = 0;
		for(int i = 0; i < 30_000; i++){
			v += System.nanoTime();
		}
		bh.consume(v);
	}

	@Benchmark
	public void testJni(Blackhole bh){
		long v = 0;
		for(int i = 0; i < 30_000; i++){
			v += PreciseTimestamp.getMicros();
		}
		bh.consume(v);
	}

	@Benchmark
	public void testCriticalJni(Blackhole bh){
		long v = 0;
		for(int i = 0; i < 30_000; i++){
			v += PreciseTimestamp.getCMicros();
		}
		bh.consume(v);
	}

	public static void main(String[] args) throws RunnerException {
		Options opt = new OptionsBuilder()
				.include(PerfTiming.class.getSimpleName())
				.warmupIterations(5)
				.measurementIterations(5)
				.build();

		new Runner(opt).run();
	}
}


и был удивлён, когда увидел результаты

Benchmark                   Mode  Cnt     Score    Error  Units
PerfTiming.testCriticalJni  avgt    5  1636.577 ± 40.370  us/op
PerfTiming.testJni          avgt    5  1673.504 ± 86.489  us/op
PerfTiming.testSystemNano   avgt    5  1179.510 ± 31.920  us/op


Т.е видно, конечно, что Critical JNI таки быстрее, но не сильно (по сравнению с System.nano)

Буду рад услышать отзыв — может быть я где-то ошибся или не так, что интерпретировал.
А что смущает? Выглядит всё логично. nanoTime() быстрее, потому что это JVM intrinsic и выполняется в контексте Java кода без переключения в натив. Critical JNI ненамного быстрее, потому что в простом методе без аргументов ему экономить особо не на чем, разве что на лишних JNIEnv и jclass.

Кроме того, сравнивать gettimeofday с nanoTime не совсем корректно, т. к. nanoTime реализован по-другому. А вот currentTimeMillis реализован как раз через gettimeofday.

Кстати, не надо в JMH бенчмарках гонять циклы — рискуете попасться на все те грабли, с которыми JMH борется. Там внутри и так есть цикл. К тому же, теряется смысл Score. Если я измеряю одну операцию, то Score даёт интуитивно понятную оценку, типа nanoTime работает около 15 наносекунд, что соответствует примерно 30 тактам 2GHz процессора. А так 1179 — просто какое-то абстрактное число.
Сравнение с nanoTime делалось чисто чтобы как-то сравнить разницу — условную, чтобы видет разницу между jni / critical jni.

Про циклы в benchmark-е — сперва было всё без циклов и там jni critical так же на чуть-чуть меньше — подумал, может быть он как-то недостаточно разогрет.

Словом, всё более-менее ожидаемо и спасибо за ответ.
В JDK 7 и 8 работают. Все бенчмарки и результаты, упомянутые в статье, были получены именно на Windows.
Sign up to leave a comment.

Articles