【问题标题】:Measuring performance of java.io.InputStream测量 java.io.InputStream 的性能
【发布时间】:2019-10-17 13:23:21
【问题描述】:

我有一个 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

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

由于 JNI 规范定义了

在临界区内,本地代码不得调用其他 JNI 函数,或任何可能导致当前线程 阻塞并等待另一个 Java 线程。 (例如,当前 线程不得在另一个 Java 写入的流上调用 read 线程。)

我认为从关键部分中的常规文件读取没有任何问题。从常规文件中读取只会被短暂阻塞,并且不依赖于任何 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,您可能应该使用 nio 来读取您的文件。
  • 任何不会导致当前线程阻塞并等待另一个Java线程;主要是因为你可以通过这种方式使 JVM 崩溃(其他线程不知道通知阻塞的本机线程)。
  • 我不会在生产代码中使用“可能不危险”的东西。你需要确定。但是......嘿......我们只能提供建议。责任在你。
  • 但这里是一个“例如”。考虑如果您正在阅读的文件位于网络共享或 NFS 服务器上会发生什么。在这种情况下,操作系统可能需要通过网络获取数据。可能需要很长时间。事实上,它甚至可能需要无限长的时间......如果服务器出现故障等。一直以来,您的代码都处于“关键区域”,并且可能会阻塞 GC 等等。
  • 即使“直接读入数组”也可能是“从底层 fs 缓冲区复制”操作,所以当您确信源是本地文件时,您可能更喜欢使用内存映射,结合复制到HeapByteBuffer。这仍然承担着不可避免的复制操作,但避免了中间DirectByteBuffer引入的额外复制操作。

标签: java performance io jvm inputstream


【解决方案1】:

带有 FileInputStream 的 2MB 缓冲区可能不是最佳选择。有关详细信息,请参阅 this question。虽然它是在 Windows 上,但我在 Linux 上看到过similar performance issue。根据操作系统,分配临时大缓冲区可能会导致额外的mmap 调用和随后的页面错误。如此大的缓冲区也使 L1/L2 缓存无用。

从常规文件读取只会被暂时阻止,不会 依赖于任何 java 线程。

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

JNI 临界区的问题 是每当发生延迟时,它可能会影响所有线程,而不仅仅是执行 I/O 的线程。这对于单线程应用程序来说不是问题,但这可能会导致多线程应用程序中出现不必要的停顿。

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

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

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

【讨论】:

  • 在您的基准测试中,文件显然缓存在操作系统页面缓存中,并且没有发生设备 I/O。这就是原因。 perf stat -e 'block:*' 没有记录到与物理设备的交互。清除缓存后,性能差异小于 10%。但是我认为即使某些进程只是在没有任何sync 的情况下写入数据,然后另一个进程将尝试读取它们,也不可能发生磁盘 I/O。 (我将其测试为手动将一些数据添加到文件中,然后使用perf stat -e 'block:*' 运行读取它的进程。没有记录交互)。
  • 值得指出的是OP从问题中的“/tmp”读取文件; 可能tmpfs(并支持RAM)。
  • @ElliottFrisch 是的。当我对其进行基准测试时,没有发生重大故障。但我认为这不是问题。
猜你喜欢
  • 2011-05-09
  • 1970-01-01
  • 1970-01-01
  • 2011-12-20
  • 2011-01-27
  • 1970-01-01
  • 1970-01-01
  • 2019-02-24
  • 2012-07-26
相关资源
最近更新 更多