Измерение производительности java.io.InputStream

У меня есть файл размером 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)
        );
    }
}

РЕЗУЛЬТАТ = 2121714428

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

Измерение производительности java.io.InputStream

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

Inside a critical region, native code must not call other JNI functions, or any system call that may cause the current thread to block and wait for another Java thread. (For example, the current thread must not call read on a stream being written by another Java thread.)

Я не вижу никаких проблем с чтением из обычный файл в критической секции. Чтение из обычного файла блокируется ненадолго и не зависит ни от какого 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 наносекунд, что почти в два раза эффективнее.

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

InputStream не буферизуется, происходит переключение контекста, а чтение файловой системы является (потенциально) блокирующей операцией. Вместо JNI вам, вероятно, следует использовать нио для чтения ваших файлов.
Elliott Frisch 01.06.2019 01:54

@ElliottFrisch Конечно, DirectByteBuffer не страдает от дополнительного копирования, поскольку он находится в куче C, и его содержимое недоступно сборщику мусора, но вопрос в том, почему мы не можем использовать критические методы при использовании byte[] и java.io?

Some Name 01.06.2019 01:55

@ElliottFrisch Меня смущает формулировка спецификации JNI. Какие системные вызовы разрешено вызывать в критических разделах...?

Some Name 01.06.2019 01:57

Любой, что не будет заставить текущий поток блокироваться и ждать другого потока Java; главным образом потому, что вы можете разбить JVM таким образом (другой поток не знает, чтобы уведомить блокирующий собственный поток).

Elliott Frisch 01.06.2019 02:14

@ElliottFrisch Учитывая наихудший сценарий, если read выполняется внутри критической области каким-то потоком, а другой поток запускается OutOfMemory. Означает ли это, что заблокированный поток не будет уведомлен и работа JVM не будет завершена (как должно было быть)?

Some Name 01.06.2019 02:22

В этом случае я подозреваю (но не проверял), что собственный поток никогда не проснется (но ОС очистит его, когда JVM завершит работу).

Elliott Frisch 01.06.2019 02:23

@ElliottFrisch Понятно, спасибо. Но, может быть, вы знаете обходной путь? Причина, по которой я не использую nio, заключается в том, что я обязан работать с byte[]. Чтение в HeapByteBuffer включает дополнительную копию из временного DirectByteBuffer, поэтому на самом деле ничего не меняет.

Some Name 01.06.2019 02:27

Использование BufferedInputStream не поможет при большом чтении. По сути, если вы не можете использовать NIO, у вас нет вариантов безопасно для ускорения чтения.

Stephen C 01.06.2019 03:33

@StephenC Я нашел статью о критических shipilev.net/jvm/anatomy-quarks/9-jni-critical-gclocker и заметил, что вызов функций JNI действительно опасен. Но, наверное, такое "критическое чтение" не так уж и опасно...

Some Name 01.06.2019 03:57

Я бы не стал использовать что-то, что «вероятно, не опасно» в производственном коде. Вы должны быть Конечно. Но... эй... мы можем только посоветовать. Ответственность лежит на вас.

Stephen C 01.06.2019 04:23

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

Stephen C 01.06.2019 04:31

Даже «чтение непосредственно в массив», скорее всего, является операцией «копирования из нижележащего буфера файловой системы», поэтому, если вы уверены, что источником является локальный файл, вы можете предпочесть использование сопоставления памяти в сочетании с копированием в HeapByteBuffer. Это все еще несет неизбежную операцию копирования, но избегает дополнительной операции копирования, введенной промежуточным звеном DirectByteBuffer.

Holger 03.06.2019 13:33

@Holger скорее всего, это «копия из базового буфера fs» Я бы сказал определенно....

Some Name 03.06.2019 14:59
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
1
13
596
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

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

Reading from a regular file is blocked only briefly and does not depend on any java thread.

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

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

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

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

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

В вашем тесте файл, по-видимому, кэшируется в кеше страницы ОС, и ввод-вывод устройства не происходит.. Это было причиной. perf stat -e 'block:*' не зафиксировано никаких взаимодействий с физическим устройством. После очистки кешей разница в производительности составила менее 10%. Но я предполагаю, что никакого дискового ввода-вывода, скорее всего, не произойдет, даже если какой-то процесс просто запишет данные без каких-либо sync, а затем другой процесс попытается их прочитать. (Я тестировал это как ручное добавление в файл некоторых данных, а затем запуск процесса чтения с помощью perf stat -e 'block:*'. Никаких взаимодействий не было зарегистрировано).
Some Name 02.06.2019 00:52

Стоит отметить, что OP читает файл из «/ tmp» в вопросе; что мощь будет tmpfs (и поддерживается оперативной памятью).

Elliott Frisch 02.06.2019 18:01

@ElliottFrisch Это так. Никаких серьезных ошибок не возникало, когда я проводил бенчмаркинг. Но я не думаю, что это проблема.

Some Name 03.06.2019 16:04

Другие вопросы по теме