衡量java.io.InputStream的性能

时间:2019-05-31 23:44:48

标签: java performance io jvm 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)
        );
    }
}

RESULT = 2121714428

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

enter image description here

因为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纳米,这几乎是效率的两倍。

问题: 从关键区域中的 常规文件 中读取数据的实际问题是什么?

1 个答案:

答案 0 :(得分:2)

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

  

从常规文件读取仅被短暂阻止,不会   取决于任何Java线程。

并非总是如此。在您的基准测试中,文件显然已缓存在OS页面缓存中,并且没有发生设备I / O。访问实际硬件(尤其是旋转磁盘)可能会慢几个数量级。磁盘I / O的最坏时间无法完全预测-可能长达数百毫秒,具体取决于硬件条件,I / O队列的长度,调度策略等。

JNI关键部分的问题是每当发生延迟时,它可能会影响所有线程,不仅影响I / O。对于单线程应用程序来说,这不是一个问题,但是这可能会导致多线程应用程序出现意外的世界停顿。

反对JNI严重的另一个原因是与GCLocker相关的JVM错误。有时,它们可能会导致多余的GC循环或忽略某些GC标志。以下是一些示例(仍未解决):

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

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