Измерение производительности java.io.InputStream - PullRequest
1 голос
/ 01 июня 2019

У меня есть файл размером 5 ГБ, который я хочу прочитать кусками, скажем, 2 МБ.Использование java.io.InputStream работает нормально.Таким образом, я измерил эту вещь следующим образом:

static final byte[] buffer = new byte[2 * 1024 * 1024];

public static void main(String args[]) throws IOException {
    while(true){
        InputStream is = new FileInputStream("/tmp/log_test.log");
        long bytesRead = 0;
        int readCurrent;
        long start = System.nanoTime();
        while((readCurrent = is.read(buffer)) > 0){
            bytesRead += readCurrent;
        }
        long end = System.nanoTime();
        System.out.println(
            "Bytes read = " + bytesRead + ". Time elapsed = " + (end - start)
        );
    }
}

RESULT = 2121714428

Видно, что в среднем требуется 2121714428 нанометров.Это связано с тем, что реализация (*env)->SetByteArrayRegion(env, bytes, off, nread, (jbyte *)buf); данных считывает в malloc ed или выделенный в стек буфер, как показано здесь .Так что memcpy занимает довольно большое количество процессорного времени:

enter image description here

Поскольку спецификация JNI определяет, что

Внутри критическойрегион, собственный код не должен вызывать другие функции JNI или любой системный вызов, который может вызвать блокировку текущего потока и ожидание другого потока Java.(Например, текущий поток не должен вызывать чтение для потока, записываемого другим потоком Java.)

Я не вижу проблем с чтением из обычногофайл в критическом разделе.Чтение из обычного файла блокируется только на короткое время и не зависит от какого-либо потока Java.Примерно так:

static final byte[] buffer = new byte[2 * 1024 * 1024];

public static void main(String args[]) throws IOException {
    while (true) {
        int fd = open("/tmp/log_test.log");
        long bytesRead = 0;
        int readCurrent;
        long start = System.nanoTime();
        while ((readCurrent = read(fd, buffer)) > 0) {
            bytesRead += readCurrent;
        }
        long end = System.nanoTime();
        System.out.println("Bytes read = " + bytesRead + ". Time elapsed = " + (end - start));
    }
}

private static native int open(String path);

private static native int read(int fd, byte[] buf);

Функции JNI:

JNIEXPORT jint JNICALL Java_com_test_Main_open
  (JNIEnv *env, jclass jc, jstring path){
    const char *native_path = (*env)->GetStringUTFChars(env, path, NULL);
    int fd = open(native_path, O_RDONLY);
    (*env)->ReleaseStringUTFChars(env, path, native_path);
    return fd;
}


JNIEXPORT jint JNICALL Java_com_test_Main_read
  (JNIEnv *env, jclass jc, jint fd, jbyteArray arr){
    size_t java_array_size = (size_t) (*env)->GetArrayLength(env, arr);
    void *buf = (*env)->GetPrimitiveArrayCritical(env, arr, NULL);
    ssize_t bytes_read = read(fd, buf, java_array_size);
    (*env)->ReleasePrimitiveArrayCritical(env, arr, buf, 0);
    return (jint) bytes_read;
}

РЕЗУЛЬТАТ = 1179852225

Выполнение этого в цикле занимает в среднем 1179852225 нано, что почти вдвое эффективнее,

Вопрос: В чем проблема с чтением из обычного файла в критическом разделе?

1 Ответ

2 голосов
/ 01 июня 2019

2 МБ буфер с FileInputStream, вероятно, не лучший выбор. См. этот вопрос для получения подробной информации. Хотя это было в Windows, я видел похожую проблему производительности в Linux. В зависимости от ОС выделение временного большого буфера может привести к дополнительным mmap вызовам и последующим ошибкам страницы. Также такой большой буфер делает кеш L1 / L2 бесполезным.

Чтение из обычного файла блокируется ненадолго и не зависит от любого потока Java.

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

Проблема с критической секцией JNI заключается в том, что всякий раз, когда происходит задержка, она может затрагивать все потоки, а не только тот, который выполняет ввод / вывод. Это не проблема для однопоточного приложения, но это может вызвать нежелательные паузы остановки мира в многопоточном приложении.

Другая причина против JNI критическая - это ошибок JVM, связанных с GCLocker . Иногда они могут вызывать избыточные циклы GC или игнорировать определенные флаги GC. Вот несколько примеров (все еще не исправлено):

  • JDK-8048556 Ненужные молодые GC, инициированные GCLocker
  • JDK-8057573 CMSScavengeBeforeRemark игнорируется, если GCLocker активен
  • JDK-8057586 Явный GC игнорируется, если GCLocker активен

Итак, вопрос в том, заботитесь ли вы о пропускной способности или латентности . Если вам нужна только более высокая пропускная способность, JNI критический, вероятно, правильный путь. Однако, если вы также заботитесь о предсказуемой задержке (не средней задержке, а, скажем, о 99,9%), тогда критический JNI не кажется хорошим выбором.

...