java.io.InputStream 的衡量表现

Measuring performance of java.io.InputStream

我有一个 5GB 大小的文件,我想按块读取,比如 2MB。使用 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

可以看出平均需要2121714428nanos。之所以如此,是因为实现确实 (*env)->SetByteArrayRegion(env, bytes, off, nread, (jbyte *)buf); 将数据读入 malloced 或堆栈分配的缓冲区,如图 here 所示。所以 memcpy 需要相当多的 CPU 时间:

由于 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 纳秒,这几乎是效率的两倍。

问题:常规文件读取的实际问题是什么在临界区内?

使用 FileInputStream 的 2MB 缓冲区可能不是最佳选择。 请参阅 Linux 上的 for details. Although it was on Windows, I've seen a similar performance issue。根据 OS,分配临时大缓冲区可能会导致额外的 mmap 调用和后续页面错误。如此大的缓冲区也使 L1/L2 缓存无用。

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

这并不总是正确的。在您的基准测试中,文件显然缓存在 OS 页面缓存中,并且没有设备 I/O 发生。访问真实硬件(尤其是旋转磁盘)可能要慢几个数量级。磁盘I/O的最坏时间是不完全可以预测的——它可以大到几百毫秒,这取决于硬件条件、I/O队列的长度、调度策略等。

JNI 临界区的问题 是每当发生延迟时,它可能会影响所有线程,而不仅仅是执行 I/O 的线程。对于 single-threaded 应用程序来说这不是问题,但这可能会导致 multi-threaded 应用程序出现不希望的 stop-the-world 暂停。

反对 JNI 关键的另一个原因是 与 GCLocker 相关的 JVM 错误。有时它们可​​能会导致冗余的 GC 周期或忽略某些 GC 标志。以下是一些示例(仍未修复):

  • JDK-8048556 不必要的 GCLocker-initiated 年轻 GC
  • JDK-8057573 如果 GCLocker 处于活动状态,则忽略 CMSScavengeBeforeRemark
  • JDK-8057586 如果 GCLocker 处于活动状态,则忽略显式 GC

所以,问题是你关心的是吞吐量还是延迟。如果您只需要更高的吞吐量,JNI critical 可能是正确的选择。但是,如果您还关心可预测的延迟(不是平均延迟,而是 99.9%),那么 JNI critical 似乎不是一个好的选择。