【问题标题】:Write "compressed" Array to increase IO performance?编写“压缩”数组以提高 IO 性能?
【发布时间】:2012-06-30 06:32:50
【问题描述】:

我有一个长度为 2.2 亿(固定)的 int 和 float 数组。现在,我想将这些数组存储/上传到内存和磁盘。目前,我正在使用 Java NIO 的 FileChannel 和 MappedByteBuffer 来解决这个问题。它工作正常,但需要大约 5 秒(挂钟时间)将阵列存储/上传到/从内存到磁盘。现在,我想让它更快。

在这里,我应该提到大多数数组元素是 0(接近 52 %)。

喜欢:

int arr1 [] = { 0 , 0 , 6 , 7 , 1, 0 , 0 ...}

任何人都可以帮助我,有什么好方法可以通过不存储或加载那些 0 来提高速度。这可以通过使用 Arrays.fill (array , 0) 来补偿。

【问题讨论】:

  • 最简单的方法是利用 java.util.zip 的内置压缩。这可能不是最佳压缩,但需要几行代码。
  • @bestsss,谢谢。但是,如何使用 zip 压缩数组并将其存储并加载到内存中?可以指点一下吗?
  • @Arpssss DeflaterOutputStream(以及相关的 GZip/Zip 流)。 Deflater 哪里瓶颈在哪里(IO/CPU?);还要确保写入流缓冲。
  • 我想我应该这样做,因为在不指定大小的情况下同时使用 DefalterOutputStream 和 GZipOutStream 会导致糟糕的性能。
  • @Arpssss 数字的范围是多少?可以是short[] 还是byte[]? (也就是说,在编码每个数字时是否有很多“浪费空间”?)

标签: java arrays io compression


【解决方案1】:

根据分布,考虑Run-length Encoding

运行长度编码 (RLE) 是一种非常简单的数据压缩形式,其中数据运行(即相同数据值出现在许多连续数据元素中的序列)存储为单个数据值和计数,而不是像原来那样运行。 这对于包含许多此类运行的数据最有用。

这很简单......这是好的,也可能是坏的,在这里;-)

【讨论】:

  • 是什么让您认为数据包含运行?例如,序列 0,1,0,2,0,3,0,4,0,5,0,6,0,7 符合 OP 的要求,但没有任何运行。
  • @meriton “取决于发行版..”也就是说,我喜欢 bestsss 关于“仅使用 ZIP”的评论。
  • 终于把它放在了zip-read的代码中,写完“写”部分后完全无聊
【解决方案2】:

如果您愿意自己编写序列化-反序列化代码,您可以存储一系列指示这些零所在位置的范围(使用特殊标记)以及实际的非零,而不是存储所有零数据。

所以你的例子中的数组: { 0 , 0 , 6 , 7 , 1, 0 , 0 ...} 可以存储为:

%0-1, 6, 7, 1 %5-6

读取此数据时,如果您点击 %,则表示您有一个范围,您读取开始和结束并填充零。然后你继续看到一个非#,这意味着你达到了一个实际值。

在具有大量连续值序列的稀疏数组中,这将产生很好的压缩效果。

【讨论】:

  • 这完全是 RLE,对 int 使用文本代码也是一个非常糟糕的主意。
  • 是的。但这会降低加载或存储速度吗?
  • @bestsss 已经发布关于使用 ZipOutputStream/DeflaterOutputStream 的答案:-/
  • 它将减少您在磁盘上占用的空间量,从而减少加载阵列所需的 i/o。当然,正如@pst 提到的,这取决于分布。
  • @pst,刚到家,德国-意大利之后我会放弃答案
【解决方案3】:

以下方法需要磁盘上的 n / 8 + nz * 4 字节,其中 n 是数组的大小,而 nz 是非零条目的数量。对于 52% 的零条目,您可以将存储大小减少 52% - 3% = 49%。

你可以这样做:

void write(int[] array) {
    BitSet zeroes = new BitSet();
    for (int i = 0; i < array.length; i++)
        zeroes.set(i, array[i] == 0);
    write(zeroes); // one bit per index
    for (int i = 0; i < array.length; i++)
        if (array[i] != 0)
            write(array[y]);
}

int[] read() {
    BitSet zeroes = readBitSet();
    array = new int[zeroes.length];
    for (int i = 0; i < zeroes.length; i++) {
        if (zeroes.get(i)) {
            // nothing to do (array[i] was initialized to 0)
        } else {
            array[i] = readInt();
        }
    }
}

编辑:你说这稍微慢一点意味着磁盘不是瓶颈。您可以通过在构建时写入位集来调整上述方法,因此您不必在将位集写入磁盘之前将其写入内存。此外,通过逐字写入位集并穿插实际数据,我们可以只对数组进行一次传递,从而减少缓存未命中:

void write(int[] array) {
    writeInt(array.length);
    int ni;
    for (int i = 0; i < array.length; i = ni) {
        ni = i + 32;
        int zeroesMap = 0;
        for (j = i + 31; j >= i; j--) {
            zeroesMap <<= 1;
            if (array[j] == 0) {
                zeroesMap |= 1;
            }
        }
        writeInt(zeroesMap);
        for (j = i; j < ni; j++)
            if (array[j] != 0) {
                writeInt(array[j]);
            }
        }
    }
}

int[] read() {
    int[] array = new int[readInt()];
    int ni;
    for (int i = 0; i < array.length; i = ni) {
        ni = i + 32;
        zeroesMap = readInt();
        for (j = i; j < ni; j++) {
            if (zeroesMap & 1 == 1) {
                // nothing to do (array[i] was initialized to 0)
            } else {
                array[j] = readInt();
            }
            zeroesMap >>= 1;
        }
    }
    return array;
}

(前面的代码假设 array.length 是 32 的倍数。如果不是,则以任何你喜欢的方式写入数组的最后一个切片)

如果这也不能减少处理时间,那么压缩不是可行的方法(我认为任何通用压缩算法都不会比上述更快)。

【讨论】:

  • 有趣的方法(+1)。虽然递归的read() 正确吗?
  • 目前,我正在使用stackoverflow.com/questions/11156945/… 代码来存储上传。该代码是否兼容?对不起,我必须得到代码。
  • @pst:是的,这很令人困惑。我现在更好地命名了辅助方法。
  • @Arpsss:您可以使用您喜欢的任何 I/O api 实现辅助方法 write(Bitset)、write(int)、readBitSet() 和 readInt()。
  • @Arpssss 获得了什么结果?只是好奇:-)
【解决方案4】:

在 java 中有一个标准的压缩工具:java.util.zip - 它是通用库,但由于纯粹的可用性是一个不错的解决方案。如果需要,应该研究专门的压缩,编码,我很少推荐 zip 作为选择的灵魂。

这是一个如何通过Deflater/Inflater 处理 zip 的示例。 大多数人都知道 ZipInput/Output Stream(尤其是 Gzip)。他们在处理来自 mem->zlib 和 esp 的副本时都有缺点。 GZip 这完全是一场灾难,因为 CRC32 调用本机代码(调用本机代码会消除优化能力并引入更多性能损失)。

几个重要的注意事项:不要将 zip 压缩率提高到高,这会破坏任何性能 - 当然,可以尝试并调整 CPU 和磁盘活动之间的最佳比率。

该代码还展示了java.util.zip 的真正缺点之一——它不支持直接缓冲区。支持是微不足道的,但没有人费心去做。直接缓冲区将节省少量内存副本并减少内存占用。

最后一点:(j)zlib 有 java 版本,它优于原生 impl。压缩效果很好。

package t1;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Random;
import java.util.zip.DataFormatException;
import java.util.zip.Deflater;
import java.util.zip.Inflater;

public class ZInt {
    private static final int bucketSize = 1<<17;//in real world should not be const, but we bored horribly
    static final int zipLevel = 2;//feel free to experiement, higher compression (5+)is likely to be total waste
    
    static void write(int[] a, File file, boolean sync) throws IOException{
        byte[] bucket = new byte[Math.min(bucketSize,  Math.max(1<<13, Integer.highestOneBit(a.length >>3)))];//128KB bucket
        byte[] zipOut = new byte[bucket.length];
        
        final FileOutputStream fout = new FileOutputStream(file);
        FileChannel channel = fout.getChannel();
        try{
            
            ByteBuffer buf = ByteBuffer.wrap(bucket);
            //unfortunately java.util.zip doesn't support Direct Buffer - that would be the perfect fit
            ByteBuffer out = ByteBuffer.wrap(zipOut);
            out.putInt(a.length);//write length aka header
            if (a.length==0){
                doWrite(channel, out, 0);
                return;
            }
            
            Deflater deflater = new Deflater(zipLevel, false);
            try{
                for (int i=0;i<a.length;){
                    i = put(a, buf, i);
                    buf.flip();
                    deflater.setInput(bucket, buf.position(), buf.limit());

                    if (i==a.length)
                        deflater.finish();

                    //hacking and using bucket here is tempting since it's copied twice but well
                    for (int n; (n= deflater.deflate(zipOut, out.position(), out.remaining()))>0;){
                        doWrite(channel, out, n);
                    }
                    buf.clear();
                }
                
            }finally{
                deflater.end();
            }
        }finally{
            if (sync) 
                fout.getFD().sync();
            channel.close();
        }
    }

    static int[] read(File file) throws IOException, DataFormatException{
        FileChannel channel = new FileInputStream(file).getChannel();
        try{
            byte[] in = new byte[(int)Math.min(bucketSize, channel.size())];
            ByteBuffer buf = ByteBuffer.wrap(in);

            channel.read(buf);
            buf.flip();
            int[] a = new int[buf.getInt()];
            if (a.length==0)
                return a;
            int i=0;
            byte[] inflated = new byte[Math.min(1<<17, a.length*4)];
            ByteBuffer intBuffer = ByteBuffer.wrap(inflated);
            Inflater inflater = new Inflater(false);
            try{
                do{
                    if (!buf.hasRemaining()){
                        buf.clear();
                        channel.read(buf);
                        buf.flip();
                    }
                    inflater.setInput(in, buf.position(), buf.remaining());
                    buf.position(buf.position()+buf.remaining());//simulate all read

                    for (;;){
                        int n = inflater.inflate(inflated,intBuffer.position(), intBuffer.remaining());
                        if (n==0)
                            break;
                        intBuffer.position(intBuffer.position()+n).flip();
                        for (;intBuffer.remaining()>3 && i<a.length;i++){//need at least 4 bytes to form an int
                            a[i] = intBuffer.getInt();
                        }
                        intBuffer.compact();
                    }

                }while (channel.position()<channel.size() && i<a.length);
            }finally{
                inflater.end();
            }
            //          System.out.printf("read ints: %d - channel.position:%d %n", i, channel.position());
            return a;
        }finally{
            channel.close();
        }
    }

    private static void doWrite(FileChannel channel, ByteBuffer out, int n) throws IOException {
        out.position(out.position()+n).flip();
        while (out.hasRemaining())
            channel.write(out);
        out.clear();
    }
    private static int put(int[] a, ByteBuffer buf, int i) {
        for (;buf.hasRemaining() && i<a.length;){
            buf.putInt(a[i++]);
        }
        return i;
    }
    
    private static int[] generateRandom(int len){
        Random r = new Random(17);
        int[] n = new int[len];
        for (int i=0;i<len;i++){
            n[i]= r.nextBoolean()?0: r.nextInt(1<<23);//limit bounds to have any sensible compression
        }
        return n;
    }
    public static void main(String[] args) throws Throwable{
        File file = new File("xxx.xxx");
        int[] n = generateRandom(3000000); //{0,2,4,1,2,3};
        long start = System.nanoTime();
        write(n, file, false);
        long elapsed = System.nanoTime() - start;//elapsed will be fairer if the sync is true
        
        System.out.printf("File length: %d, for %d ints, ratio %.2f in %.2fms %n", file.length(), n.length, ((double)file.length())/4/n.length, java.math.BigDecimal.valueOf(elapsed, 6) );
        
        int[] m = read(file);
        
        //compare, Arrays.equals doesn't return position, so it sucks/kinda
        for (int i=0; i<n.length; i++){
            if (m[i]!=n[i]){
                System.err.printf("Failed at %d%n",i);
                break;
            }
        }
        System.out.printf("All done!");
    };
    
}

请注意,代码不是正确的基准!
延迟回复是因为代码很无聊,又是一个 zip 示例,抱歉

【讨论】:

  • +1 虽然,我实际上并没有期望/要求一个完整的示例,而实际上只是“使用此类”或“在此处指定大小”或“注意这个”.. ;)
  • @pst,我注意到我放错了 并且格式被切断了。它可以归因于如此松散的解析/缺乏警告。通常我不会停留在简单的案例上并发布完全有效的解决方案或什么都不发布。
  • @bestsss,非常感谢。上面的例子对我来说真的很好。但是,当我尝试将其转换为读写 Long 数组时,它会出错。您能否再次提供帮助,在上面的代码中如何将其修改为读写 Long 数组。另一个问题:我正在为 2.2 亿长度的数组做这件事,我应该为此更改存储桶大小等参数吗?
  • 长数组,如 java.lang.Long (!),如果你使用得更好,请确保使用原语,否则更改应该是微不足道的。
  • @bestsss,是的,我使用的是 long [] 数组 = new long []。代码在stackoverflow.com/questions/11368343/… 中给出。你能检查一下吗?
猜你喜欢
  • 2010-09-30
  • 2011-07-11
  • 2013-04-17
  • 2014-07-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多