TIPS:此篇文章信息来源于:
CONCURRENCY AND COMPUTATION: PRACTICE AND EXPERIENCE期刊杂志
作者:Dan Bonachea, Phillip Dickens and Rajeev Thakur
前言:
人们对使用Java作为开发高性能计算应用程序的语言越来越感兴趣。然而,要在高性能计算领域取得成功,Java不仅能够提供高计算性能,还必须能够提供高性能的I/O。在本文中,我们首先研究了几种尝试在Java中提供高性能I/O的方法——其中许多方法乍一看并不明显——并评估了它们在两台并行机器上的性能
在C语言中执行并行读和写也非常简单,而不需要同步(在支持这种访问的文件系统上)。特别是,每个进程都可以寻找共享随机访问文件的独立(不重叠)区域,然后并行地对文件的不相交区域执行读写
然而,在Java中的情况却完全不同。在Java中实现高性能的并行文件I/O目前是一个非常困难的问题,接下来我们进行讨论
在java中使用并行文件i/o的方法
在本节中,我们将描述在Java中执行并行文件I/O的六种不同方法。这些方法大多是解决Java不直接支持读取或写除字节之外的任何数据类型的数组的问题的不同方法
1.使用原始字节数组
byte buf[] = new byte[buf_size];
RandomAccessFile raf = new RandomAccessFile(filename, access);
raf.seek(position);
raf.write(buf, my_start_buf, num_bytes);
2.用原始的方式转换成字节数组
for (int i=0; i < int_array.length; i++) {
int_array[i] = (((int)buf[4*i+3]) & 255)
| ( (((int)buf[4*i+2]) & 255) << 8 )
| ( (((int)buf[4*i+1]) & 255) << 16 )
| ( (((int)buf[4*i+0]) & 255) << 24 );
}
3.使用数据流
int[] int_array = new int[num_ints];
RandomAccessFile raf = new RandomAccessFile(filename,access);
raf.seek(position);
for (int i = start_buf; i < (start_buf+num_ints_to_write); i++){
raf.writeInt(int_array[i]);
}
4.使用缓冲数据流
RandomAccessFile raf = new RandomAccessFile(filename,access);
FileDescriptor fd = raf.getFD();
FileOutputStream fos = new FileOutputStream(fd);
BufferedOutputStream bos= new BufferedOutputStream(fos);
DataOutputStream dos = new DataOutputStream(bos);
raf.seek(position);
for (int i = start_buf; i < (start_buf + num_ints_to_write); i++)
dos.writeInt(int_array[i]);
5.对字节数组流使用缓冲区
RandomAccessFile raf = new RandomAccessFile(filename,access);
ByteArrayOutputStream bos = new ByteArrayOutputStream(size);
DataOutputStream dos = new DataOutputStream(bos);
for(int i = start_buf; i < (start_buf + num_ints_to_write); i++)
dos.writeInt(int_array[i]);
raf.seek(position);
raf.write(bos.toByteArray());
6.其他方法
在Java中,至少还有另外两种执行I/O的方法。
一种方法是使用对象序列化。我们最初研究了这种方法,但发现Java向文件中添加了一些额外的字节,以存储与对象相关的信息。这使得执行并行读或写变得困难,因为线程不知道在文件中查找的位置。Java中的对象序列化也被认为是非常慢的。
另一种方法是不使用Java中定义的I/O方法,而是使用Java本机接口(JNI),用专门处理基于数组的I/O的新方法扩展现有库。我们使用这种方法来实现本文中提出的批量I/O扩展。
两台不同机器的运行结果分析(y轴为带宽,x轴为线程数量)
总结
这些实验基本上可以分为两类:
第一类,它使用JavaI/O方法来读取/写入字节数组。在这类类别的第一种情况下,我们假设数据已经以字节形式存在;
在第二种情况下(编码/解码),我们显式地执行从整数数组到字节数组的转换,反之亦然。第二个类别包括所有其他实验,它单独使用数据流类,或者链接到一些提供缓冲的底层流。
当使用数据流类和方法时,I/O性能相当差,即使是在缓冲时。
数据流类的性能不佳源于三个因素。
首先,在不使用缓冲的情况下,这种方法需要对数组中的每个元素调用I/O子系统。当I/O需求很小时,这可能是可以接受的,但对于大型I/O密集型应用程序肯定是不可接受的。
其次,即使由底层流提供了缓冲区,这种方法也仍然需要为数组中的每个元素调用一个方法。使用64个进程和一个1Gbyte数组,每个进程必须对读入或写入的方法进行超过400万次的调用。使用单个进程,这个数字将增加到超过268 000 000。显然,这是实现高性能文件I/O的一个重大障碍。
第三个问题是,数据输出流类的许多方法一次写入底层流一个字节,并且每个这样的写入都需要一个synchronized锁获取。
尽管缓冲将数据流的性能提高了一个数量级(例如,从0.00074兆字节/s到0.19兆字节/s),但它不能直接匹配编写字节数组的性能,即超过100兆字节/s。我们还观察到,当使用缓冲区数据流时,缓冲区的大小非常重要。特别是,选择正确的缓冲区大小是吞吐量的两倍以上。(我们还应该注意到,需要大量的实验来找到最好的缓冲区大小。)然而,性能上的差异仅在1兆字节/s到3兆字节/s的范围内。
正如预期的那样,在使用JavaI/O工具直接读写字节数组时获得了最佳的性能。事实上,第一种方法,只是假设数据已经在字节形式,提供性能本质上相同时使用C。然而,有一个性能显著下降(除了一个实验)当应用程序本身必须转换数据从数组整数字节数组或反之亦然(编码/解码)。有了这种方法,一个实际执行非平凡计算的更现实的应用程序可能会看到更大的性能下降——在我们的测试程序中,CPU可以或多或少地完全用于执行编码/解码转换,而不会降低整体运行时间。
原始数组带宽下降原因
在SP上的一个显著结果是,当使用原始字节数组从32个处理器移动到64个处理器时,观察到的性能有了相当显著的下降。此下降的原因是配置不足的I/O子系统只有四个I/O处理器引起的争用。这种趋势在其他方法中观察到,因为它们没有在接近硬件限制的带宽下运行。使用32个处理器的原始字节数组获得最佳写性能(导致带宽为106兆节/s)。编码/解码的最佳结果是20兆字节/s和64个处理器。在所有其他方法中观察到的最大吞吐量是7.5兆节/s,通过64个处理器并使用字节数组流进行缓冲获得。
当使用具有16个处理器的原始字节数组时,为读取操作获得的最佳性能是96兆字节/s。当处理器的数量增加到32个和64个时,性能略有下降,这再次是由于I/O子系统的配置不足。使用编码/解码获得的最佳性能是30兆字节/s和64个处理器。所有数据流方法的最佳性能是7.5兆字节/s,同样是通过64个处理器获得的,并使用字节数组流进行缓冲