为什么Java磁盘I/O比用C编写的相应I/O代码慢得多?

4

我有一块固态硬盘,根据规格书应该提供不少于10k IOPS。我的基准测试证实它可以给我20k IOPS。

然后我创建了如下测试:

private static final int sector = 4*1024;
private static byte[] buf = new byte[sector];
private static int duration = 10; // seconds to run
private static long[] timings = new long[50000];
public static final void main(String[] args) throws IOException {
    String filename = args[0];
    long size = Long.parseLong(args[1]);
    RandomAccessFile raf = new RandomAccessFile(filename, "r");
    Random rnd = new Random();
    long start = System.currentTimeMillis();
    int ios = 0;
    while (System.currentTimeMillis()-start<duration*1000) {
        long t1 = System.currentTimeMillis();
        long pos = (long)(rnd.nextDouble()*(size>>12));
        raf.seek(pos<<12);
        int count = raf.read(buf);
        timings[ios] = System.currentTimeMillis() - t1;
        ++ios;
    }
    System.out.println("Measured IOPS: " + ios/duration);
    int totalBytes = ios*sector;
    double totalSeconds = (System.currentTimeMillis()-start)/1000.0;
    double speed = totalBytes/totalSeconds/1024/1024;
    System.out.println(totalBytes+" bytes transferred in "+totalSeconds+" secs ("+speed+" MiB/sec)");
    raf.close();
    Arrays.sort(timings);
    int l = timings.length;
    System.out.println("The longest IO = " + timings[l-1]);
    System.out.println("Median duration = " + timings[l-(ios/2)]);
    System.out.println("75% duration = " + timings[l-(ios * 3 / 4)]);
    System.out.println("90% duration = " + timings[l-(ios * 9 / 10)]);
    System.out.println("95% duration = " + timings[l-(ios * 19 / 20)]);
    System.out.println("99% duration = " + timings[l-(ios * 99 / 100)]);
}

然后我运行这个示例,只获得2186 IOPS:

$ sudo java -cp ./classes NioTest /dev/disk0 240057409536
Measured IOPS: 2186
89550848 bytes transferred in 10.0 secs (8.540234375 MiB/sec)
The longest IO = 35
Median duration = 0
75% duration = 0
90% duration = 0
95% duration = 0
99% duration = 0

为什么它的速度比同样的C测试慢那么多?
更新:这里是Python代码,可以提供20k IOPS:
def iops(dev, blocksize=4096, t=10):

    fh = open(dev, 'r')
    count = 0
    start = time.time()
    while time.time() < start+t:
        count += 1
        pos = random.randint(0, mediasize(dev) - blocksize) # need at least one block left
        pos &= ~(blocksize-1)   # sector alignment at blocksize
        fh.seek(pos)
        blockdata = fh.read(blocksize)
    end = time.time()
    t = end - start
    fh.close()

更新2:NIO 代码(只是一部分,不会重复所有方法)
...
RandomAccessFile raf = new RandomAccessFile(filename, "r");
InputStream in = Channels.newInputStream(raf.getChannel());
...
int count = in.read(buf);
...

2
你在Java和C中使用相同的随机数序列吗?请注意,原始磁盘传输速度无关紧要。对于随机访问,您需要查看寻道时间。 - Patricia Shanahan
4
为什么往我的口袋 USB 驱动器中写入 40000 个 .java 文件需要 8 分钟,而只有一个我剪辑的 mp4 视频(总大小相同)只需 20 秒?我要求退回我购买 USB 驱动器的钱。 - AsConfused
3
请发布用C语言编写的同一测试代码,以便读者可以确信正在进行的比较。 - Dan Getz
3
有趣的是这个类被命名为NioTest但没有包含任何NIO代码。虽然有足够的证据表明使用NIO并不能保证速度提升,但我仍然希望看到相同的测试使用FileChannel完成,甚至可以使用MappedByteBuffer,因为问题声称Java本身存在缺陷。 - VGR
2
这不会影响您所询问的 IOPS 结果,但是使用 System.nanoTime() 而不是 currentTimeMillis() 可以让您更精确地测量各个时间。 - Dan Getz
显示剩余10条评论
4个回答

7
你的问题基于一个错误的假设,即类似于你的Java代码的C代码将能够像IOMeter一样表现良好。因为这个假设是错误的,所以不存在需要解释C性能和Java性能差异的情况。
如果你的问题是,为什么相对于IOMeter来说,你的Java代码表现得如此之差,那么答案是IOMeter并不像你的代码一样逐个发出请求。为了获取SSD的完整性能,你需要保持其请求队列非空闲状态,等待每个读取完成后再发出下一个请求是无法实现这一点的。
尝试使用一个线程池来发出你的请求。

4

这篇文章是关于编程的,日期有些旧,其中提到遗留的Java随机访问速度较慢,大约比C/C++慢2.5到3.5倍。这是一份研究报告,请不要因为点击了它而责怪我。

链接: http://pages.cs.wisc.edu/~guo/projects/736.pdf

相比于C/C++,Java原始I/O速度较慢,因为Java中的系统调用更加昂贵;缓存可以提高Java I/O性能,因为它减少了系统调用,但对于较大的缓存大小并没有太大的收益;直接缓存优于Java提供的缓冲I/O类,因为用户可以根据自己的需求进行定制;增加操作大小可以在没有开销的情况下提高I/O性能;在Java本地方法中,系统调用很便宜,而调用本地方法的开销相当高。如果适当减少本地调用的数量,则可以实现与C/C++相当的性能。

你的代码来自那个时代。现在让我们重新编写它,不使用RandomAccessFile,而是使用java.nio好吗?

我有一些nio2代码,我们可以与C进行比较。垃圾回收可以排除 :)


我相信我做错了什么,但就是想不出来哪里有问题。我尝试了NIO,但IOPS还是一样的。如果您能提供替代代码,将不胜感激。 - Anthony
它不会更快,但也不会慢3.5倍。 - Drew
我以前只做汇编和C语言,所以我不是妄想狂。好吧,大多数情况下不是。 - Drew

1

因为你在使用RandomAccessFile,这是Java中最慢的磁盘I/O方法之一。

尝试使用更快的东西,比如BufferedInputStream或者BufferedOutputStream,看看速度有什么变化。

如果你想知道为什么这会对SSD(因为SSD应该非常擅长随机访问)有影响,那跟访问的随机性无关,而是跟带宽有关。如果你有一个1024位宽的总线的SSD,但你每次只写64位(就像通过写longdouble那样),那么你会得到很慢的速度。(当然,这些数字仅供示例目的。)

现在我可以看出来,你的代码并不是在做这个(或者至少看起来不是这样),但是有可能RandomAccessFile在底层实现时是这样的。再次尝试使用缓冲流,看看会发生什么。


1
自从什么时候开始,BufferedInputStream 需要 2 TB 的内存了? - Sam Estep
1
@Antonio 是的,我知道。但是,您没有指定10k IOPS规格是用于顺序访问还是随机访问,或者您的C I/O测试的详细信息。 - Sam Estep
1
@Antonio,恐怕我不相信你。请发布你的C基准测试代码。 - Sam Estep
2
@Antonio,您在SO上提出了一个问题,任何人都可以帮助您。如果您不同意某个答案或认为它是错误的,请将其投票否决并继续前进。不要请求删除答案。此外,请不要在评论或问题标题中表现粗鲁和好斗。 - royhowie
5
@Antonio - 有答案并不会阻止你获得另一个答案。对回答你的人无礼是不好的。请理解RedRoboHood志愿花费自己的时间来帮助你。 - Brad Larson
显示剩余10条评论

1

拙劣的文章。非常糟糕的结果,会引导人产生误解。在提问之前我已经阅读了它。 - Anthony

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接