【问题标题】:Speeding up file read加速文件读取
【发布时间】:2025-12-26 09:25:20
【问题描述】:

我有一个 1.7G 的文件,格式如下:

String Long String Long String Long String Long ... etc

本质上,String 是一个键,Long 是一个哈希图中的值,我有兴趣在我的应用程序中运行其他任何东西之前对其进行初始化。

我当前的代码是:

  RandomAccessFile raf=new RandomAccessFile("/home/map.dat","r");
                raf.seek(0);
                while(raf.getFilePointer()!=raf.length()){
                        String name=raf.readUTF();
                        long offset=raf.readLong();
                        map.put(name,offset);
                }

这大约需要 12 分钟才能完成,我确信有更好的方法可以做到这一点,因此我将不胜感激任何帮助或指点。

谢谢


按照 EJP 建议更新?

EJP 感谢您的建议,我希望这就是您的意思。如有不对请指正

DataInputStream dis=null;
    try{
     dis=new DataInputStream(new BufferedInputStream(new FileInputStream("/home/map.dat")));
     while(true){
       String name=dis.readUTF();
       long offset=dis.readLong();
       map.put(name, offset);
     }
    }catch (EOFException eofe){
      try{
        dis.close();
      }catch (IOException ioe){
        ioe.printStackTrace();
      }
    }

【问题讨论】:

  • 您的分析结果说明了什么?瓶颈到底在哪里?
  • 1.7G键值对,为什么不用数据库而不是文件?
  • 你想用这么多数据做什么?我有一种强烈的感觉,您可能使用了一种低效的方法。
  • @Perception 我不知道什么是“连续直播”,但他已经非常充分地指定了文件格式,无论是口头还是通过他的代码。
  • "live" = "line" 我猜。

标签: java file file-io io random-access


【解决方案1】:
  1. 使用包裹在 FileInputStream 周围的 BufferedInputStream 的 DataInputStream。

  2. 每次迭代至少需要四个系统调用,检查长度和当前大小并执行谁知道要获得字符串和 long 的读取次数,只需调用 readUTF() 和 readLong() 直到您得到一个 EOFException。

【讨论】:

  • 感谢 EJP 的回答和 cmets。我已经尝试过了,上传数据大约需要 5 分钟。我使用了 DataInputStream 但我没有等待 EOFExcpetion 而是使用了可用的 sys 调用,这可能会减慢读取过程。
  • @DotNet 确实如此。您每次迭代都添加了一个系统调用。试试我的方法。我的方式是每 8k 数据只有一个系统调用。差别很大。
  • 总时间 4 分 22 秒没有系统调用“可用”。再次感谢
  • @DotNet 它应该比这快:不到一分钟,包括构建地图。听起来您没有按照建议使用BufferedInputStream。我的计时始终显示数百个 RandomAccessFile/DataInputStream/BufferedInputStream 的比率,这是 RAF 版本的第二个计时,以使其受益于任何缓存。
  • 我已更新问题以包含您的建议。如果这不是您的意思,请告诉我。
【解决方案2】:

我会构建文件,以便它可以就地使用。即不以这种方式加载。由于您有可变长度的记录,您可以构建每个记录位置的数组,然后按顺序放置键,以便您可以对数据执行二进制搜索。 (或者您可以使用自定义哈希表)然后您可以使用隐藏数据实际存储在文件中而不是转换为数据对象的事实的方法包装它。

如果你做了所有这些,“加载”阶段就变得多余了,你就不需要创建这么多的对象了。


这是一个很长的例子,但希望能说明什么是可能的。

import vanilla.java.chronicle.Chronicle;
import vanilla.java.chronicle.Excerpt;
import vanilla.java.chronicle.impl.IndexedChronicle;
import vanilla.java.chronicle.tools.ChronicleTest;

import java.io.IOException;
import java.util.*;

public class Main {
    static final String TMP = System.getProperty("java.io.tmpdir");

    public static void main(String... args) throws IOException {
        String baseName = TMP + "/test";
        String[] keys = generateAndSave(baseName, 100 * 1000 * 1000);

        long start = System.nanoTime();
        SavedSortedMap map = new SavedSortedMap(baseName);
        for (int i = 0; i < keys.length / 100; i++) {
            long l = map.lookup(keys[i]);
//            System.out.println(keys[i] + ": " + l);
        }
        map.close();
        long time = System.nanoTime() - start;

        System.out.printf("Load of %,d records and lookup of %,d keys took %.3f seconds%n",
                keys.length, keys.length / 100, time / 1e9);
    }

    static SortedMap<String, Long> generateMap(int keys) {
        SortedMap<String, Long> ret = new TreeMap<>();
        while (ret.size() < keys) {
            long n = ret.size();
            String key = Long.toString(n);
            while (key.length() < 9)
                key = '0' + key;
            ret.put(key, n);
        }
        return ret;
    }

    static void saveData(SortedMap<String, Long> map, String baseName) throws IOException {
        Chronicle chronicle = new IndexedChronicle(baseName);
        Excerpt excerpt = chronicle.createExcerpt();
        for (Map.Entry<String, Long> entry : map.entrySet()) {
            excerpt.startExcerpt(2 + entry.getKey().length() + 8);
            excerpt.writeUTF(entry.getKey());
            excerpt.writeLong(entry.getValue());
            excerpt.finish();
        }
        chronicle.close();
    }

    static class SavedSortedMap {
        final Chronicle chronicle;
        final Excerpt excerpt;
        final String midKey;
        final long size;

        SavedSortedMap(String baseName) throws IOException {
            chronicle = new IndexedChronicle(baseName);
            excerpt = chronicle.createExcerpt();
            size = chronicle.size();
            excerpt.index(size / 2);
            midKey = excerpt.readUTF();
        }

        // find exact match or take the value after.
        public long lookup(CharSequence key) {
            if (compareTo(key, midKey) < 0)
                return lookup0(0, size / 2, key);
            return lookup0(size / 2, size, key);
        }

        private final StringBuilder tmp = new StringBuilder();

        private long lookup0(long from, long to, CharSequence key) {
            long mid = (from + to) >>> 1;
            excerpt.index(mid);
            tmp.setLength(0);
            excerpt.readUTF(tmp);
            if (to - from <= 1)
                return excerpt.readLong();
            int cmp = compareTo(key, tmp);
            if (cmp < 0)
                return lookup0(from, mid, key);
            if (cmp > 0)
                return lookup0(mid, to, key);
            return excerpt.readLong();
        }

        public static int compareTo(CharSequence a, CharSequence b) {
            int lim = Math.min(a.length(), b.length());
            for (int k = 0; k < lim; k++) {
                char c1 = a.charAt(k);
                char c2 = b.charAt(k);
                if (c1 != c2)
                    return c1 - c2;
            }
            return a.length() - b.length();
        }

        public void close() {
            chronicle.close();
        }
    }

    private static String[] generateAndSave(String baseName, int keyCount) throws IOException {
        SortedMap<String, Long> map = generateMap(keyCount);
        saveData(map, baseName);
        ChronicleTest.deleteOnExit(baseName);

        String[] keys = map.keySet().toArray(new String[map.size()]);
        Collections.shuffle(Arrays.asList(keys));
        return keys;
    }
}

生成 2 GB 的原始数据并执行一百万次查找。它以这样一种方式编写,即加载和查找使用很少的堆。 (

ls -l /tmp/test*
-rw-rw---- 1 peter peter 2013265920 Dec 11 13:23 /tmp/test.data
-rw-rw---- 1 peter peter  805306368 Dec 11 13:23 /tmp/test.index

/tmp/test created.
/tmp/test, size=100000000
Load of 100,000,000 records and lookup of 1,000,000 keys took 10.945 seconds

使用哈希表查找每次查找会更快,因为它是 O(1) 而不是 O(ln N),但实现起来更复杂。

【讨论】:

  • +1 这加上一个内存映射文件应该可以完美结合性能、初始化时间和内存消耗。
  • 如果 OP 无法更改文件,可以通过创建具有这种结构的索引文件来实施这种方法。
  • @MarkoTopolnik 我很难将其描述为“完美”。比 OP 现在所做的更多的内存、更多的 I/O 和更多的执行时间。是的,启动时间更短。
  • 谢谢彼得。我已经实现了这一点,这次我通过将字符串转换为整数来使用固定大小的记录。
  • @EJP 它是 1.7 GB,如果选择占用那么多堆与系统缓存,系统缓存是更好且更具可扩展性的选择,特别是如果条目的访问频率不同。像这样的一大堆意味着你的主要 GC 变成了一个可怕的拖累。此外,将内存映射文件 I/O 视为任何 I/O 也没有多大意义。