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

是什么让 Stream Read/Write 如此长的令人难以置信?作为一个读/写测试,对我来说,一遍又一遍地读取相同的整数看起来有点毫无意义(如果我很好地理解 中发生了什么Stream Read/Write)从先前写入的文件中读取 int 不是更好吗?在同一个地方读写整数?有没有更好的方法来说明它?

一段时间以来,我一直对很多这些事情感到头疼,但我无法了解整个情况。

4

3 回答 3

2

我在一个基准“流读/写”中看到的是:

  • 它并不真正进行流 I/O,而是寻找文件中的特定位置。这是非缓冲的,因此所有 I/O 都必须从磁盘完成(其他流正在使用缓冲 I/O,因此实际上是在大块中读/写,然后从内存区域读取或写入整数)。
  • 它正在寻找结束 - 4 个字节,因此读取最后一个 int 并写入一个新 int。每次迭代,文件的长度都会继续增长一个 int。不过,这确实不会增加太多时间成本(但确实表明该基准的作者要么误解了某些东西,要么不小心)。

这解释了该特定基准的非常高的成本。

您问:

从先前写入的文件中读取 int 并在同一个地方读取和写入 int 不是更好吗?

这就是我认为作者试图对最后两个基准做的事情,但这不是他们得到的。要在RandomAccessFile文件中读取和写入相同的位置,您需要在读取和写入之前进行查找:

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 由 OS 内存管理器处理,该管理器将使用其页面大小。页面大小总是被优化以很好地映射到文件块。

在该示例中,内存映射测试确实将所有内容读入内存缓冲区,然后将其全部写回。这两个测试真的写得不好来比较这两种情况。memmoryMappedCopy应该以与customBufferedCopy.

编辑:这些测试类甚至可能有更多问题。由于您对另一个答案的评论,我再次仔细查看了第一堂课。

方法customBufferedCopy是静态的并使用静态缓冲区。对于这种测试,应该在方法中定义缓冲区。然后它就不需要使用synchronized(尽管它在这种情况下和这些测试中都不需要它)。这种静态方法被称为普通方法,这是不好的编程习惯(即使用FileCopy.customBufferedCopy(...)代替new FileCopy().customBufferedCopy(...))。

如果您确实从多个线程运行此测试,那么该缓冲区的使用将是有争议的,并且基准测试不仅仅与文件 I/O 有关,因此比较两种测试方法的结果是不公平的。

于 2010-04-07T21:24:16.983 回答
0

1)这些听起来像是你的学生应该问的问题——而不是相反?

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

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

4)关于最后一个问题,我得走了——所以我希望其他人可以帮助你。

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

于 2010-04-07T16:01:37.017 回答
0

感谢您查看这个。稍后我将查看第一个示例,现在,我的教授要求重写 2 个测试(流和映射读/写)
它们生成随机整数,首先读取索引(生成的整数)并检查该索引处的整数等于生成的 int,如果不相等,生成的 int 将写入其索引处。他认为这可能会导致更好的测试,更多地使用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+)来说,它需要很长时间。我只是尝试了一些东西,但我不确定我是否走在正确的轨道上。

于 2010-04-09T09:50:50.047 回答