【问题标题】:Storing a very large set of numbers in Java在 Java 中存储大量数字
【发布时间】:2021-12-26 09:30:00
【问题描述】:

我正在尝试存储一组范围从 0 到 ~600 亿的数字,其中该集合开始为空并逐渐变得更密集,直到它包含该范围内的每个数字。该集合不必能够删除数字。目前我的方法是将集合表示为一个非常长的布尔数组并将该数组存储在文本文件中。我为此创建了一个类,并测试了 RandomAccessFile 和 FileChannel,其数字范围限制在 0 到 20 亿之间,但在这两种情况下,该类在添加和查询数字方面都比使用常规布尔数组慢得多。 这是我班级的当前状态:

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.*;
public class FileSet {
    private static final int BLOCK=10_000_000;
    private final long U;
    private final String fname;
    private final FileChannel file;
    public FileSet(long u, String fn) throws IOException {
        U=u;
        fname=fn;
        BufferedOutputStream out=new BufferedOutputStream(new FileOutputStream(fname));
        long n=u/8+1;
        for (long rep=0; rep<n/BLOCK; rep++) out.write(new byte[BLOCK]);
        out.write(new byte[(int)(n%BLOCK)]);
        out.close();
        file=new RandomAccessFile(fn,"rw").getChannel();
    }
    public void add(long v) throws IOException {
        if (v<0||v>=U) throw new RuntimeException(v+" out of range [0,"+U+")");
        file.position(v/8);
        ByteBuffer b=ByteBuffer.allocate(1); file.read(b);
        file.position(v/8);
        file.write(ByteBuffer.wrap(new byte[] {(byte)(b.get(0)|(1<<(v%8)))}));
    }
    public boolean has(long v) throws IOException {
        if (v<0||v>=U) return false;
        file.position(v/8);
        ByteBuffer b=ByteBuffer.allocate(1); file.read(b);
        return ((b.get(0)>>(v%8))&1)!=0;
    }
    public static void main(String[] args) throws IOException {
        long U=2000_000_000;
        SplittableRandom rnd=new SplittableRandom(1);
        List<long[]> actions=new ArrayList<>();
        for (int i=0; i<1000000; i++) actions.add(new long[] {rnd.nextInt(2),rnd.nextLong(U)});

        StringBuilder ret=new StringBuilder(); {
            System.out.println("boolean[]:");
            long st=System.currentTimeMillis();
            boolean[] b=new boolean[(int)U];
            System.out.println("init time="+(System.currentTimeMillis()-st));
            st=System.currentTimeMillis();
            for (long[] act:actions)
                if (act[0]==0) b[(int)act[1]]=true;
                else ret.append(b[(int)act[1]]?"1":"0");
            System.out.println("query time="+(System.currentTimeMillis()-st));
        }

        StringBuilder ret2=new StringBuilder(); {
            System.out.println("FileSet:");
            long st=System.currentTimeMillis();
            FileSet fs=new FileSet(U,"FileSet/"+U+"div8.txt");
            System.out.println("init time="+(System.currentTimeMillis()-st));
            st=System.currentTimeMillis();
            for (long[] act:actions) {
                if (act[0]==0) fs.add(act[1]);
                else ret2.append(fs.has(act[1])?"1":"0");
            }
            System.out.println("query time="+(System.currentTimeMillis()-st));
            fs.file.close();
        }
        if (!ret.toString().equals(ret2.toString())) System.out.println("MISMATCH");
    }
}

和输出:

boolean[]:
init time=1248
query time=148
FileSet:
init time=269
query time=3014

此外,当范围从 20 亿增加到 100 亿时,查询的总运行时间会有很大的跳跃,尽管理论上总运行时间应该大致保持不变。当我单独使用该类时(因为布尔数组不再适用于这么大的范围),查询时间从 ~3 秒到 ~50 秒。当我将范围增加到 600 亿时,时间增加到 ~240 秒。 我的问题是:是否有更快的方法来访问和修改任意索引处的超大文件?是否有一种完全不同的方法来存储比我目前的方法更快的大型整数集?

【问题讨论】:

  • 是一组键值对吗?
  • 如果你的设置超过了物理内存的大小,那么虚拟内存被用来存储映射的一部分,当访问一个位时它可能需要一个磁盘加载。除了购买更多内存或更快的磁盘驱动器之外,没有什么可做的了。 600 亿对于随机访问来说是一个巨大的数字,而您已经处于机器的极限。
  • 是的,你可以存储非常多的集合数据。
  • @markspace 600 亿字节是 60 GB,可以放入内存。 600 亿位是除以 8,即 7.5 GB,现在可以轻松放入内存中。
  • 您可能希望将en.wikipedia.org/wiki/Bloom_filter 作为检查集合成员资格的第一阶段...

标签: java io set nio


【解决方案1】:

事实证明,最简单的解决方案是使用 64 位 JVM 并通过在终端中运行我的 Java 程序并使用 -Xmx10g 之类的标志来增加 Java 堆空间。然后我可以简单地使用longs 的数组来隐式存储整个集合。

【讨论】:

    【解决方案2】:

    Boolean 数组是一种非常低效的信息存储方式,因为每个布尔值占用 8 位。您应该改用BitSet。但是 BitSet 也有 20 亿的限制,因为它使用原始 int 值作为参数(并且Integer.MAX_VALUE 限制了内部长数组的大小)。

    一种跨越超过 20 亿个条目的高效内存替代方案是创建您自己的 BitSet 包装器,将数据拆分为子集并为您进行索引:

    public class LongBitSet {
        // TODO: Initialize array and add error checking.
        private final BitSet bitSets = new BitSet[64];
        public void set(long index) {
            bitSets[(int) (index / Integer.MAX_VALUE)]
                .set((int) (index % Integer.MAX_VALUE));
        }
    }
    

    但也有其他选择。如果您有非常密集的数据,则使用游程编码将是一种增加内存容量的廉价方法。但这可能会涉及 B-tree 结构,以提高访问效率。这些只是指针。做出正确答案的很多因素仅取决于您实际使用数据结构的方式。

    【讨论】:

    • 在使用超大文件的类中,我实际上使用每个字节来表示集合中 8 个数字的成员/非成员,所以当范围设置为 ~600 亿时,实际我使用的文件大小只有 ~7.5 GB(我在我的文件中使用扩展的 ASCII)。另外,我的集合一开始是完全空的,然后逐渐变得更密集,直到它包含 0 到 ~600 亿范围内的每个数字,所以我不确定运行长度编码 + B 树会提高多少性能。
    猜你喜欢
    • 1970-01-01
    • 2015-12-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-03-26
    • 1970-01-01
    • 2016-09-13
    相关资源
    最近更新 更多