传统IO与内存映射技术的比较

4
我希望能向学生们说明Java中传统IO和内存映射文件之间性能差异的区别。我在网上找到了一个例子,但并不是所有步骤都很清晰,我甚至认为有些步骤是不必要的。我在这里阅读了很多相关资料,但对于它们的正确实现都没有信心。
我试图理解的代码如下:
public class FileCopy{
    public static void main(String args[]){
        if (args.length < 1){
            System.out.println(" Wrong usage!");
            System.out.println(" Correct usage is : java FileCopy <large file with full path>");
            System.exit(0);
        }


        String inFileName = args[0];
        File inFile = new File(inFileName);

        if (inFile.exists() != true){
            System.out.println(inFileName + " does not exist!");
            System.exit(0);
        }

        try{
            new FileCopy().memoryMappedCopy(inFileName, inFileName+".new" );
            new FileCopy().customBufferedCopy(inFileName, inFileName+".new1");
        }catch(FileNotFoundException fne){
            fne.printStackTrace();
        }catch(IOException ioe){
            ioe.printStackTrace();
        }catch (Exception e){
            e.printStackTrace();
        }


    }

    public void memoryMappedCopy(String fromFile, String toFile ) throws Exception{
        long timeIn = new Date().getTime();
        // read input file
        RandomAccessFile rafIn = new RandomAccessFile(fromFile, "rw");
        FileChannel fcIn = rafIn.getChannel();
        ByteBuffer byteBuffIn = fcIn.map(FileChannel.MapMode.READ_WRITE, 0,(int) fcIn.size());
        fcIn.read(byteBuffIn);
        byteBuffIn.flip();

        RandomAccessFile rafOut = new RandomAccessFile(toFile, "rw");
        FileChannel fcOut = rafOut.getChannel();

        ByteBuffer writeMap = fcOut.map(FileChannel.MapMode.READ_WRITE,0,(int) fcIn.size());

        writeMap.put(byteBuffIn);   

        long timeOut = new Date().getTime();
        System.out.println("Memory mapped copy Time for a file of size :" + (int) fcIn.size() +" is "+(timeOut-timeIn));
        fcOut.close();
        fcIn.close();
    }


    static final int CHUNK_SIZE = 100000;
    static final char[] inChars = new char[CHUNK_SIZE];

    public static void customBufferedCopy(String fromFile, String toFile) throws IOException{
        long timeIn = new Date().getTime();

        Reader in = new FileReader(fromFile);
        Writer out = new FileWriter(toFile);
        while (true) {
            synchronized (inChars) {
                int amountRead = in.read(inChars);
                if (amountRead == -1) {
                    break;
                }
                out.write(inChars, 0, amountRead);
            }
        }
        long timeOut = new Date().getTime();
        System.out.println("Custom buffered copy Time for a file of size :" + (int) new File(fromFile).length() +" is "+(timeOut-timeIn));
        in.close();
        out.close();
    }
}

什么时候需要使用RandomAccessFile?在这里,它被用来读写memoryMappedCopy,是否真的需要完全复制文件?或者这是内存映射的一部分?

customBufferedCopy中,为什么要在这里使用synchronized

我还发现了一个不同的例子,应该测试两者之间的性能:

public class MappedIO {
    private static int numOfInts = 4000000;
    private static int numOfUbuffInts = 200000;
    private abstract static class Tester {
        private String name;
        public Tester(String name) { this.name = name; }
        public long runTest() {
            System.out.print(name + ": ");
            try {
                long startTime = System.currentTimeMillis();
                test();
                long endTime = System.currentTimeMillis();
                return (endTime - startTime);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        public abstract void test() throws IOException;
    }
    private static Tester[] tests = { 
        new Tester("Stream Write") {
            public void test() throws IOException {
                DataOutputStream dos = new DataOutputStream(
                        new BufferedOutputStream(
                                new FileOutputStream(new File("temp.tmp"))));
                for(int i = 0; i < numOfInts; i++)
                    dos.writeInt(i);
                dos.close();
            }
        }, 
        new Tester("Mapped Write") {
            public void test() throws IOException {
                FileChannel fc = 
                    new RandomAccessFile("temp.tmp", "rw")
                .getChannel();
                IntBuffer ib = fc.map(
                        FileChannel.MapMode.READ_WRITE, 0, fc.size())
                        .asIntBuffer();
                for(int i = 0; i < numOfInts; i++)
                    ib.put(i);
                fc.close();
            }
        }, 
        new Tester("Stream Read") {
            public void test() throws IOException {
                DataInputStream dis = new DataInputStream(
                        new BufferedInputStream(
                                new FileInputStream("temp.tmp")));
                for(int i = 0; i < numOfInts; i++)
                    dis.readInt();
                dis.close();
            }
        }, 
        new Tester("Mapped Read") {
            public void test() throws IOException {
                FileChannel fc = new FileInputStream(
                        new File("temp.tmp")).getChannel();
                IntBuffer ib = fc.map(
                        FileChannel.MapMode.READ_ONLY, 0, fc.size())
                        .asIntBuffer();
                while(ib.hasRemaining())
                    ib.get();
                fc.close();
            }
        }, 
        new Tester("Stream Read/Write") {
            public void test() throws IOException {
                RandomAccessFile raf = new RandomAccessFile(
                        new File("temp.tmp"), "rw");
                raf.writeInt(1);
                for(int i = 0; i < numOfUbuffInts; i++) {
                    raf.seek(raf.length() - 4);
                    raf.writeInt(raf.readInt());
                }
                raf.close();
            }
        }, 
        new Tester("Mapped Read/Write") {
            public void test() throws IOException {
                FileChannel fc = new RandomAccessFile(
                        new File("temp.tmp"), "rw").getChannel();
                IntBuffer ib = fc.map(
                        FileChannel.MapMode.READ_WRITE, 0, fc.size())
                        .asIntBuffer();
                ib.put(0);
                for(int i = 1; i < numOfUbuffInts; i++)
                    ib.put(ib.get(i - 1));
                fc.close();
            }
        }
    };
    public static void main(String[] args) {
        for(int i = 0; i < tests.length; i++)
            System.out.println(tests[i].runTest());
    }
}

我大致了解正在发生的事情,我的输出结果如下:

Stream Write: 653
Mapped Write: 51
Stream Read: 651
Mapped Read: 40
Stream Read/Write: 14481
Mapped Read/Write: 6

什么导致流读写如此漫长?作为读/写测试,如果我正确理解了流读写的运作方式,一遍又一遍地读取相同的整数似乎有点毫无意义。是不是从先前写入的文件中读取int,然后在同一位置读取和写入int更好?有没有更好的方法来说明这个问题?
我已经为这些事情苦苦思索了很长时间,但总体情况始终难以理清。
3个回答

2
我在“Stream Read/Write”基准测试中看到的情况是:
  • 它并没有真正地进行流式输入/输出,而是寻找文件中的特定位置。这是非缓存的,因此所有的I/O都必须从磁盘完成(其他流使用缓存I/O,因此实际上是从内存区域读取/写入大块数据,然后再读取/写入整数)。
  • 它正在寻找末尾-4个字节,所以读取最后一个整数,然后写入一个新的整数。每次迭代,文件的长度会增加一个整数,但这确实不会增加太多的时间成本(但表明该基准测试的作者可能误解了某些内容或者不够谨慎)。

这解释了这个特定基准测试的非常高的成本。

你问道:

读取先前写入的文件中的int,然后只需在同一位置读取和写入int,这样不是更好吗?

我认为,这就是作者试图在最后两个基准测试中做的事情,但他们没有得到想要的结果。使用RandomAccessFile在文件中读取和写入相同的位置,你需要在读取和写入之前放置一个seek:

raf.seek(raf.length() - 4);
int val = raf.readInt();
raf.seek(raf.length() - 4);
raf.writeInt(val);

这确实展示了内存映射I/O的一个优点,因为您可以使用相同的内存地址访问文件的相同位,而无需在每次调用之前执行额外的查找。
顺便说一下,您的第一个基准测试示例类也可能存在问题,因为CHUNK_SIZE不是文件系统块大小的整数倍。通常最好使用1024的倍数,8192已被证明是大多数应用程序的最佳性能(这也是Java的BufferedInputStreamBufferedOutputStream为什么使用该值作为默认缓冲区大小的原因)。操作系统将需要读取额外的块以满足不在块边界上的读取请求。随后的读取(流的)将重新读取相同的块,可能是一些完整的块,然后再次读取额外的块。内存映射I/O总是按块物理读取和写入,因为实际的I/O由操作系统内存管理器处理,该管理器将使用其页面大小。页面大小总是经过优化以很好地映射到文件块。
在该示例中,内存映射测试确实将所有内容都读入内存缓冲区,然后将所有内容写回。这两个测试都没有很好地编写以比较这两种情况。memmoryMappedCopy应该使用与customBufferedCopy相同的块大小进行读取和写入。
编辑:这些测试类可能还有更多问题。由于您对其他答案的评论,我再次仔细查看了第一个类。
方法customBufferedCopy是静态的并使用静态缓冲区。对于这种测试,该缓冲区应在方法内部定义。然后它将不需要使用synchronized(虽然在这种情况下和这些测试中它不需要)。调用此静态方法作为普通方法是不好的编程实践(即使用FileCopy.customBufferedCopy(...)而不是new FileCopy().customBufferedCopy(...))。
如果您实际上从多个线程运行此测试,则使用该缓冲区将具有争议,并且基准测试不仅关于文件I/O,因此比较两个测试方法的结果是不公平的。

0

感谢您的关注。我稍后会查看第一个示例,现在,我的教授要求重新编写两个测试(流和映射读/写)
它们生成随机整数,首先读取索引(生成的整数),并检查该索引处的整数是否等于生成的整数,如果不相等,则将生成的整数写入其索引。他认为这可能会产生更好的测试,更多地利用RandomAccessFile,这有意义吗?

然而,我有一些问题,首先我不知道如何在使用RandomAccessFile时使用缓冲区进行流读/写,我找到了很多关于使用数组的byte[]缓冲区的内容,但我不确定如何正确使用它。
到目前为止,我为此测试编写的代码:

    new Tester("Stream Read/Write") {
        public void test() throws IOException {
            RandomAccessFile raf = new RandomAccessFile(new File("temp.tmp"), "rw");
            raf.seek(numOfUbuffInts*4);
            raf.writeInt(numOfUbuffInts);
            for (int i = 0; i < numOfUbuffInts; i++) {
                int getal = (int) (1 + Math.random() * numOfUbuffInts);
                raf.seek(getal*4);
                if (raf.readInt() != getal) {
                    raf.seek(getal*4);
                    raf.writeInt(getal);
                }
            }
            raf.close();
        }
    },

所以这仍然是未缓冲的...

我进行的第二次测试如下:

    new Tester("Mapped Read/Write") {
        public void test() throws IOException {
            RandomAccessFile raf = new RandomAccessFile(new File("temp.tmp"), "rw");
            raf.seek(numOfUbuffInts*4);
            raf.writeInt(numOfUbuffInts);
            FileChannel fc = raf.getChannel();
            IntBuffer ib = fc.map(FileChannel.MapMode.READ_WRITE, 0, fc.size()).asIntBuffer();

            for(int i = 1; i < numOfUbuffInts; i++) {
                int getal = (int) (1 + Math.random() * numOfUbuffInts);
                if (ib.get(getal) != getal) {
                    ib.put(getal, getal);
                }
            }
            fc.close();
        }
    }

对于小数量的numOfUbuffInts,它似乎很快,但对于大数量(20,000,000+),它需要很长时间。 我尝试了一些东西,但不确定是否正确。


0

1) 这些问题听起来应该是你的学生在问,而不是反过来吧?

2) 使用这两种方法的原因是为了展示复制文件的不同方式。我猜测第一种方法(RamdomAccessFile)会在RAM中创建一个文件版本,然后将其复制到磁盘上的新版本,而第二种方法(customBufferedCop)则直接从驱动器中读取。

3) 我不确定,但我认为synchronized用于确保同一类的多个实例不会同时写入。

4) 至于最后一个问题,我得走了 - 所以我希望其他人能够帮助你。

但说真的,这些问题听起来就像导师应该教给他们学生的问题。如果你没有能力自己研究这样简单的事情,那你给你的学生树立了什么样的榜样呢?</rant>


1
Seidre:理解的更好方式何在?难道不就是寻求帮助吗? - Geoff
我没有很好地解释我的情况(我认为这并不重要,问题就是问题,对吧?) 我正在为一位信息系统教授做实习。在操作系统课程中,他决定用Java演示一些原则。他自己没有真正的Java背景(但仍然知道相当多),但没有时间进行实验。由我来改进/解释他的例子。作为一名学生,我还有很多东西要学习,还不理解所有内容...这就是为什么我在这里提问的原因。 无论如何,感谢您的尝试。该类不是多线程的,所以我不明白为什么要使用synchronized? - Senne
除非作者还在另一个类中测试多线程并使用了该方法,否则没有理由在那里同步。但是这样做会有很多问题——该方法不应该是静态的,缓冲区也不应该是静态的。该缓冲区应该在该测试方法内部定义。我已经更新了我的答案。 - Kevin Brock

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