У меня есть файл размером 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 занимает довольно много процессорного времени:
Поскольку спецификация 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 наносекунд, что почти в два раза эффективнее.
Вопрос: В чем проблема чтения из обычный файл в критической секции?
@ElliottFrisch Конечно, DirectByteBuffer не страдает от дополнительного копирования, поскольку он находится в куче C, и его содержимое недоступно сборщику мусора, но вопрос в том, почему мы не можем использовать критические методы при использовании byte[] и java.io?
@ElliottFrisch Меня смущает формулировка спецификации JNI. Какие системные вызовы разрешено вызывать в критических разделах...?
Любой, что не будет заставить текущий поток блокироваться и ждать другого потока Java; главным образом потому, что вы можете разбить JVM таким образом (другой поток не знает, чтобы уведомить блокирующий собственный поток).
@ElliottFrisch Учитывая наихудший сценарий, если read выполняется внутри критической области каким-то потоком, а другой поток запускается OutOfMemory. Означает ли это, что заблокированный поток не будет уведомлен и работа JVM не будет завершена (как должно было быть)?
В этом случае я подозреваю (но не проверял), что собственный поток никогда не проснется (но ОС очистит его, когда JVM завершит работу).
@ElliottFrisch Понятно, спасибо. Но, может быть, вы знаете обходной путь? Причина, по которой я не использую nio, заключается в том, что я обязан работать с byte[]. Чтение в HeapByteBuffer включает дополнительную копию из временного DirectByteBuffer, поэтому на самом деле ничего не меняет.
Использование BufferedInputStream не поможет при большом чтении. По сути, если вы не можете использовать NIO, у вас нет вариантов безопасно для ускорения чтения.
@StephenC Я нашел статью о критических shipilev.net/jvm/anatomy-quarks/9-jni-critical-gclocker и заметил, что вызов функций JNI действительно опасен. Но, наверное, такое "критическое чтение" не так уж и опасно...
Я бы не стал использовать что-то, что «вероятно, не опасно» в производственном коде. Вы должны быть Конечно. Но... эй... мы можем только посоветовать. Ответственность лежит на вас.
Но вот "например". Подумайте, что произойдет, если файл, который вы читаете, находится в сетевой папке или на сервере NFS. В этом случае ОС может потребоваться получить данные по сети. Это может занять много времени. Действительно, это может занять неопределенно долгое время... если сервер выйдет из строя и т. д. Все это время ваш код находится в «критической области» и потенциально блокирует сборщик мусора и т. д.
Даже «чтение непосредственно в массив», скорее всего, является операцией «копирования из нижележащего буфера файловой системы», поэтому, если вы уверены, что источником является локальный файл, вы можете предпочесть использование сопоставления памяти в сочетании с копированием в HeapByteBuffer. Это все еще несет неизбежную операцию копирования, но избегает дополнительной операции копирования, введенной промежуточным звеном DirectByteBuffer.
@Holger скорее всего, это «копия из базового буфера fs» Я бы сказал определенно....




Буфер размером 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. Иногда они могут вызывать избыточные циклы сборки мусора или игнорирование определенных флагов сборки мусора. Вот несколько примеров (все еще не исправлено):
Итак, вопрос в том, заботитесь ли вы о пропускная способность или задержка. Если вам нужна только более высокая пропускная способность, JNI Critical, вероятно, является правильным выбором. Однако, если вы также заботитесь о предсказуемой задержке (не средней задержке, а, скажем, 99,9%), тогда критический JNI не кажется хорошим выбором.
perf stat -e 'block:*' не зафиксировано никаких взаимодействий с физическим устройством. После очистки кешей разница в производительности составила менее 10%. Но я предполагаю, что никакого дискового ввода-вывода, скорее всего, не произойдет, даже если какой-то процесс просто запишет данные без каких-либо sync, а затем другой процесс попытается их прочитать. (Я тестировал это как ручное добавление в файл некоторых данных, а затем запуск процесса чтения с помощью perf stat -e 'block:*'. Никаких взаимодействий не было зарегистрировано).
Стоит отметить, что OP читает файл из «/ tmp» в вопросе; что мощь будет tmpfs (и поддерживается оперативной памятью).
@ElliottFrisch Это так. Никаких серьезных ошибок не возникало, когда я проводил бенчмаркинг. Но я не думаю, что это проблема.
InputStreamне буферизуется, происходит переключение контекста, а чтение файловой системы является (потенциально) блокирующей операцией. Вместо JNI вам, вероятно, следует использовать нио для чтения ваших файлов.