为什么MappedByteBuffer的array()方法无法正常工作?

11

我非常新手Java,并尝试使用Mathematica的Java接口使用内存映射访问文件(希望能提高性能)。

我有的Mathematica代码(我相信)等同于以下Java代码(基于此链接):

import java.io.FileInputStream;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class MainClass {
  private static final int LENGTH = 8*100;

  public static void main(String[] args) throws Exception {
    MappedByteBuffer buffer = new FileInputStream("test.bin").getChannel().map(FileChannel.MapMode.READ_ONLY, 0, LENGTH);
    buffer.load();
    buffer.isLoaded(); // returns false, why?
  }
}

我想在缓冲区上使用array()方法,因此我试图先使用load()将缓冲区内容加载到内存中。但是,即使load()之后,isLoaded()仍返回falsebuffer.array()会抛出异常:java.lang.UnsupportedOperationException at java.nio.ByteBuffer.array(ByteBuffer.java:940)

为什么缓冲区没有加载,我该如何调用array()方法?

我的最终目标是使用asDoubleBuffer().array()获取一个double数组。虽然方法getDouble()可以正常工作,但我希望一次完成以获得良好的性能。我做错了什么?


由于我是从Mathematica进行操作的,因此我也会发布实际使用的Mathematica代码(相当于Java中的上述代码):

Needs["JLink`"]
LoadJavaClass["java.nio.channels.FileChannel$MapMode"]
buffer = JavaNew["java.io.FileInputStream", "test.bin"]@getChannel[]@map[FileChannel$MapMode`READUONLY, 0, 8*100]

buffer@load[]
buffer@isLoaded[] (* returns False *)

“返回false值并不一定意味着缓冲区的内容不在物理内存中。” load仅尽最大努力加载数据,实际上可能仅将数据加载到物理内存中,但随即被交换出去。 - Tom Hawtin - tackline
1
array 仅适用于由数组支持的缓冲区(通常来自 *Buffer.wrap)。 - Tom Hawtin - tackline
@Szabolcs J/Link在其操作中使用MathLink。因此,通过J/Link将文件导入Mathematica的速度不可能比使用Mathlink更快,因为Mathlink本身可能会引入相当大的开销。如果我正确理解您提出问题的原因,主要问题不是.mx文件的加载时间(我很难想象有什么东西能够超过.mx的加载速度),而是它们的粗略粒度。如果每个大的.mx文件只需要被加载一次(在这种情况下,这种粗略粒度就足够了),那么这应该不会有太大的影响。如果不是这样,我会创建一个类似于文件系统的... - Leonid Shifrin
@Leonid(当然是用C语言,不是Java!) - Szabolcs
@Szabolcs 是的,我明白你的问题出自何处。对于大文件,我仍然会基于小的 .mx 文件集群来处理 - 因为这样我们可以重复使用已经放入 .mx 技术中的所有工作,并享受 .mx 文件的所有通用性。需要编写一个“文件系统”,再加上一个转换器,可以将一个单独的大型数值文件自动转换成一堆 .mx 文件。这种混合方法的性能也可以做得相当好,我相信。无论是否容易编写快速的转换器,在不加载完整原始数值... - Leonid Shifrin
显示剩余12条评论
2个回答

5
根据Javadoc的说明,“映射字节缓冲区的内容随时可能发生更改,例如如果该程序或其他程序更改了映射文件相应区域的内容。无论是否发生此类更改,以及何时发生更改,都取决于操作系统并因此未指定。”
“映射字节缓冲区的全部或部分内容可能在任何时候变得无法访问,例如如果映射文件被截断。尝试访问一个不可访问的映射字节缓冲区的区域将不会更改缓冲区的内容,并将导致在访问时或稍后某个时间引发未指定的异常。因此,强烈建议采取适当的预防措施来避免通过此程序或同时运行的程序操纵映射文件,除非只是读取或写入文件的内容。”
对我来说,这似乎存在太多条件和不良行为。你需要特别使用这个类吗?
如果您只需要以最快的方式读取文件内容,请尝试:
FileChannel fChannel = new FileInputStream(f).getChannel();
    byte[] barray = new byte[(int) f.length()];
    ByteBuffer bb = ByteBuffer.wrap(barray);
    bb.order(ByteOrder.LITTLE_ENDIAN);
    fChannel.read(bb);

它的速度几乎等同于磁盘系统测试速度。

如果需要双倍速度,您可以使用DoubleBuffer(使用双倍长度为f.length()/4的double []数组),或者只调用ByteBuffer的getDouble(int)方法。


我不需要这个具体的类。 我希望能够以比内置的Mathematica功能提供的更快的方式,将非常大的二进制文件的部分内容(而不是全部)作为双精度数组获取。 - Szabolcs
更新的答案是:“对于double类型,您可以使用DoubleBuffer(使用f.length()/4大小的double[]数组)或只需调用ByteBuffer的getDouble(int)方法。” 您说您只会读取一些double。我建议使用ByteBuffer以避免将不需要的字节转换为double(在使用DoubleBuffer的情况下可能会发生这种情况)。 - andrey
谢谢@andrey。我最终确实需要获得一个double [],因为这是自动转换回Mathematica对象的内容(即我不能只使用某个索引的getDouble)。如果我尝试直接读入DoubleBuffer,它不起作用。如果我读入ByteBuffer,它可以工作,并且我可以将其视为asDoubleBuffer(),但以这种方式获取的内容再次返回false,即我无法将其转换为普通的double数组。您有任何关于如何获得双精度数组而不需要明确循环整个过程并逐个复制它们的建议吗? - Szabolcs
3
好的,我用.asDoubleBuffer().get(anArray)让它运行起来了,但是对于大文件来说速度非常慢,所以我放弃尝试在Java中完成这个任务(因为我几乎没有Java知识,而且已经花费了太长时间)。谢谢你的帮助! - Szabolcs
抱歉回复晚了。我刚刚发现ByteBuffer的默认字节顺序是BIG_ENDIAN。但是为了读取double类型,我们需要LITTLE_ENDIAN。也许已经太晚了,但无论如何,我已经使用“bb.order(ByteOrder.LITTLE_ENDIAN);”修复了代码。更多有关转换的内容请查看https://dev59.com/gGw15IYBdhLWcg3wqNgA - andrey
谢谢@andrey,确实我没有得到与另一个程序写出的相同结果,但我想:没关系,总是可以使用Java 编写相同的内容,字节序将匹配。但不幸的是,由于Java和Mathematica之间的数据传输(即使Java本身的读取不慢),这种方法注定会很慢。 - Szabolcs

0

在Java中:

final byte[] hb;                  // Non-null only for heap buffers

因此,它甚至没有针对MappedByteBuffer实现,但是有针对HeapByteBuffer的实现。

在Android中:

**
     * Child class implements this method to realize {@code array()}.
     *
     * @see #array()
     */
    abstract byte[] protectedArray();

再次强调,不是在MappedByteBuffer中,而是例如ByteArrayBuffer实现了后备数组。

 @Override byte[] protectedArray() {
    if (isReadOnly) {
      throw new ReadOnlyBufferException();
    }
    return backingArray;
  }

内存映射的重点在于它是堆外的。支持数组则会在堆上。
如果您可以从RandomAccessFile打开FileChannel,然后在通道上调用map,您还可以使用MappedByteBuffer上的批量get()方法读取到byte[]中。这将从堆外复制,避免了IO,并再次进入堆。
buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
byte[] b = new byte[buffer.limit()];
buffer.get(b);

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