代码之家  ›  专栏  ›  技术社区  ›  Durandal

有效地从byte[]数组中提取任意长度的位序列

  •  12
  • Durandal  · 技术社区  · 15 年前

    我在寻找最有效的方法来提取任意长度(0<=length<=16)的(无符号)位序列。skeleton类显示了我当前的实现如何处理这个问题:

    public abstract class BitArray {
    
    byte[] bytes = new byte[2048];
    int bitGet;
    
    public BitArray() {
    }
    
    public void readNextBlock(int initialBitGet, int count) {
        // substitute for reading from an input stream 
        for (int i=(initialBitGet>>3); i<=count; ++i) {
            bytes[i] = (byte) i;
        }
        prepareBitGet(initialBitGet, count);
    }
    
    public abstract void prepareBitGet(int initialBitGet, int count);
    
    public abstract int getBits(int count);
    
    static class Version0 extends BitArray {
        public void prepareBitGet(int initialBitGet, int count) {
            bitGet = initialBitGet;
        }
    
        public int getBits(int len) {
            // intentionally gives meaningless result
            bitGet += len;
            return 0;
        }
    }
    
    static class Version1 extends BitArray {
        public void prepareBitGet(int initialBitGet, int count) {
            bitGet = initialBitGet - 1;
        }
    
        public int getBits(int len) {
            int byteIndex = bitGet;
            bitGet = byteIndex + len;
            int shift = 23 - (byteIndex & 7) - len;
            int mask = (1 << len) - 1;
            byteIndex >>= 3;
            return (((bytes[byteIndex] << 16) | 
                   ((bytes[++byteIndex] & 0xFF) <<  8) |
                    (bytes[++byteIndex] & 0xFF)) >> shift) & mask;
        }
    }
    
    static class Version2 extends BitArray {
        static final int[] mask = { 0x0, 0x1, 0x3, 0x7, 0xF, 0x1F, 0x3F, 0x7F, 0xFF,
                    0x1FF, 0x3FF, 0x7FF, 0xFFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF };
    
        public void prepareBitGet(int initialBitGet, int count) {
            bitGet = initialBitGet;
        }
    
        public int getBits(int len) {
            int offset = bitGet;
            bitGet = offset + len;
            int byteIndex = offset >> 3; // originally used /8
            int bitIndex = offset & 7;   // originally used %8
            if ((bitIndex + len) > 16) {
                return ((bytes[byteIndex] << 16 |
                        (bytes[byteIndex + 1] & 0xFF) << 8 |
                        (bytes[byteIndex + 2] & 0xFF)) >> (24 - bitIndex - len)) & mask[len];
            } else if ((offset + len) > 8) {
                return ((bytes[byteIndex] << 8 |
                        (bytes[byteIndex + 1] & 0xFF)) >> (16 - bitIndex - len)) & mask[len];
            } else {
                return (bytes[byteIndex] >> (8 - offset - len)) & mask[len];
            }
        }
    }
    
    static class Version3 extends BitArray {
        int[] ints = new int[2048];
    
        public void prepareBitGet(int initialBitGet, int count) {
            bitGet = initialBitGet;
            int put_i = (initialBitGet >> 3) - 1;
            int get_i = put_i;
            int buf;
            buf = ((bytes[++get_i] & 0xFF) << 16) |
                  ((bytes[++get_i] & 0xFF) <<  8) |
                   (bytes[++get_i] & 0xFF);
            do {
                buf = (buf << 8) | (bytes[++get_i] & 0xFF);
                ints[++put_i] = buf;
            } while (get_i < count);
        }
    
        public int getBits(int len) {
            int bit_idx = bitGet;
            bitGet = bit_idx + len;
            int shift = 32 - (bit_idx & 7) - len;
            int mask = (1 << len) - 1;
            int int_idx = bit_idx >> 3;
            return (ints[int_idx] >> shift) & mask;
        }
    }
    
    static class Version4 extends BitArray {
        int[] ints = new int[1024];
    
        public void prepareBitGet(int initialBitGet, int count) {
            bitGet = initialBitGet;
            int g = initialBitGet >> 3;
            int p = (initialBitGet >> 4) - 1;
            final byte[] b = bytes;
            int t = (b[g]  <<  8) | (b[++g] & 0xFF);
            final int[] i = ints;
            do {
                i[++p] = (t = (t << 16) | ((b[++g] & 0xFF) <<8) | (b[++g] & 0xFF));
            } while (g < count);
        }
    
        public int getBits(final int len) {
            final int i;
            bitGet = (i = bitGet) + len;
            return (ints[i >> 4] >> (32 - len - (i & 15))) & ((1 << len) - 1);
        }
    }
    
    public void benchmark(String label) {
        int checksum = 0;
        readNextBlock(32, 1927);
        long time = System.nanoTime();
        for (int pass=1<<18; pass>0; --pass) {
            prepareBitGet(32, 1927);
            for (int i=2047; i>=0; --i) {
                checksum += getBits(i & 15);
            }
        }
        time = System.nanoTime() - time;
        System.out.println(label+" took "+Math.round(time/1E6D)+" ms, checksum="+checksum);
        try { // avoid having the console interfere with our next measurement
            Thread.sleep(369);
        } catch (InterruptedException e) {}
    }
    
    public static void main(String[] argv) {
        BitArray test;
        // for the sake of getting a little less influence from the OS for stable measurement
        Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
        while (true) {
            test = new Version0();
            test.benchmark("no implementaion");
            test = new Version1();
            test.benchmark("Durandal's (original)");
            test = new Version2();
            test.benchmark("blitzpasta's (adapted)");
            test = new Version3();
            test.benchmark("MSN's (posted)");
            test = new Version4();
            test.benchmark("MSN's (half-buffer modification)");
            System.out.println("--- next pass ---");
        }
    }
    }
    

    更高效的解决方案(性能方面)


    我上面的第一个问题似乎还不够清楚。一个N位的“位序列”形成了一个N位的整数,我需要以最小的开销提取这些整数。我不使用字符串,因为这些值要么用作查找索引,要么直接输入到某些计算中。因此,基本上,上面显示的骨架是一个真正的类,getBits()签名显示了其余代码如何与它交互。


    /八 >>三 %八 具有 &


    对于如何工作和做什么似乎有点困惑。示例代码的第一个原始post包含一个read()方法,用于指示字节缓冲区的填充位置和时间。当代码被转换成microbench时,这就丢失了。我重新介绍它是为了让它更清楚一点。 其思想是通过添加另一个子类BitArray来击败所有现有版本,后者需要实现getBits()和prepareBitGet(),后者可能是空的。 不要为了给你的解决方案带来优势而改变基准,对所有现有的解决方案都可以这样做,这是一个完全没有意义的优化!(真的!!)

    0不执行任何操作,但不执行任何操作。它总是返回0以粗略了解基准开销有多大。只是为了比较。

    此外,还添加了对MSN想法的改编(版本3)。为了保证所有竞争对手的公平性和可比性,字节数组填充现在是基准测试的一部分,也是一个准备步骤(见上文)。最初MSN的解决方案做得不太好,在准备int[]缓冲区时有很多开销。我擅自对步骤进行了一点优化,使之成为一个激烈的竞争对手:)


    结论 (上面的代码示例更新为包括基于所有适用贡献的版本)。在我的旧AMD盒(Sun JRE 1.6.0_21)上,它们显示为:

    5384 女士
    10283
    V2闪电面食(改编)的 12212 女士
    V3 MSN的(已发布)获取 11030
    V4 MSN(半缓冲区修改)需要 9700 女士

    注意:在这个基准测试中,每次调用getBits()时平均获取7.5位,每个位只读取一次。由于V3/V4必须支付很高的初始化成本,因此它们往往在获取更多、更短的情况下表现出更好的运行时行为(因此,越接近平均获取大小16的最大值,就越糟糕)。不过,V4仍然略领先于 情节。

    5 回复  |  直到 15 年前
        1
  •  2
  •   MSN    15 年前

    好吧,根据你想让时间与内存的锯齿形下降多远,你可以在每16位偏移量处每32位分配一个侧表,然后根据16位偏移量进行掩码和移位:

    byte[] bytes = new byte[2048];   
    int bitGet;   
    unsigned int dwords[] = new unsigned int[2046];
    
    public BitArray() {   
        for (int i=0; i<bytes.length; ++i) {   
            bytes[i] = (byte) i;   
        }   
    
        for (int i= 0; i<dwords.length; ++i) {
            dwords[i]= 
                (bytes[i    ] << 24) | 
                (bytes[i + 1] << 16) | 
                (bytes[i + 2] <<  8) | 
                (bytes[i + 3]);
        }
    }   
    
    int getBits(int len)
    {
        int offset= bitGet;
        int offset_index= offset>>4;
        int offset_offset= offset & 15;
    
        return (dwords[offset_index] >> offset_offset) & ((1 << len) - 1);
    }
    

    您可以避免分支(以将内存占用翻两番为代价)。而且查找面具真的比(1<<len)-1快得多吗?

        2
  •  4
  •   blizpasta    15 年前

    如果你只想把无符号位序列作为整型。

    static final int[] lookup = {0x0, 0x1, 0x3, 0x7, 0xF, 0x1F, 0x3F, 0x7F, 0xFF, 0x1FF, 0x3FF, 0x7FF, 0xFFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF };
    
    /*
     * bytes: byte array, with the bits indexed from 0 (MSB) to (bytes.length * 8 - 1) (LSB)
     * offset: index of the MSB of the bit sequence.
     * len: length of bit sequence, must from range [0,16].
     * Not checked for overflow
     */
    static int getBitSeqAsInt(byte[] bytes, int offset, int len){
    
        int byteIndex = offset / 8;
        int bitIndex = offset % 8;
        int val;
    
        if ((bitIndex + len) > 16) {
            val = ((bytes[byteIndex] << 16 | bytes[byteIndex + 1] << 8 | bytes[byteIndex + 2]) >> (24 - bitIndex - len)) & lookup[len];
        } else if ((offset + len) > 8) {
            val = ((bytes[byteIndex] << 8 | bytes[byteIndex + 1]) >> (16 - bitIndex - len)) & lookup[len];
        } else {
            val = (bytes[byteIndex] >> (8 - offset - len)) & lookup[len];
        }
    
        return val;
    }
    

    如果你想把它作为一个字符串(修改Margus的答案)。

    static String getBitSequence(byte[] bytes, int offset, int len){
    
        int byteIndex = offset / 8;
        int bitIndex = offset % 8;
        int count = 0;
        StringBuilder result = new StringBuilder();        
    
        outer:
        for(int i = byteIndex; i < bytes.length; ++i) {
            for(int j = (1 << (7 - bitIndex)); j > 0; j >>= 1) {
                if(count == len) {
                    break outer;
                }                
                if((bytes[byteIndex] & j) == 0) {
                    result.append('0');
                } else {
                    result.append('1');
                }
                ++count;
            }
            bitIndex = 0;
        }
        return  result.toString();
    }   
    
        3
  •  1
  •   Margus    15 年前

    只是想知道为什么你不能用 java.util.BitSet;

    基本上你能做的就是把整个数据 byte[] ,将其转换为二进制 string .substring() 做这项工作。这也会起作用 bit sequences > 16 .

    假设你有3个字节: 1, 2, 3 你要从第5位到第16位提取位序列。

    二进制数

    1      00000001
    2      00000010
    3      00000011
    

    public static String getRealBinary(byte[] input){
        StringBuilder sb = new StringBuilder();
    
        for (byte c : input) {
            for (int n =  128; n > 0; n >>= 1){
                if ((c & n) == 0)
                    sb.append('0');
                else sb.append('1');
            }
        }
    
        return sb.toString();
    }
    public static void main(String[] args) {
        byte bytes[] = new byte[]{1,2,3};
        String sbytes = getRealBinary(bytes);
        System.out.println(sbytes);
        System.out.println(sbytes.substring(5,16));
    }
    

    输出:

    000000010000001000000011
    00100000010
    

    速度:

    我做了个测试 1米 ,所以它相当快:

    自己重复测试的代码:

    public static void main(String[] args) {
        Random r = new Random();
        byte bytes[] = new byte[4];
        long start, time, total=0;
    
        for (int i = 0; i < 1000000; i++) {
            r.nextBytes(bytes);
            start = System.currentTimeMillis();
            getRealBinary(bytes).substring(5,16);
            time = System.currentTimeMillis() - start;
            total+=time;
        }
        System.out.println("It took " +total + "ms");
    }
    
        4
  •  0
  •   Ira Baxter    15 年前

    这里有一个可能的解决方案:

        int GetBits(int bit_index, int bit_length) {
              int byte_offset = bit_index >> 3;
              return ((((((byte_array[byte_offset]<<8)
                        +byte_array[byte_offset+1])<<8)
                        +byte_array[byte_offset+2]))
                       >>(24-(bit_index&7)+bit_length))))
                      &((1<<bit_length)-1);
             }
    

    [未经测试]

        5
  •  0
  •   Lii bob    8 年前

    从Java 7开始 BitSet 拥有 toLongArray 方法,我相信它会完全满足问题的要求:

    int subBits = (int) bitSet.get(lowBit, highBit).toLongArray()[0];
    

    这样做的好处是它可以处理大于int或long的序列。它的性能缺点是 必须分配一个新的数组对象来保存结果。