Java提供了四种类型来存储一个整型:Byte,short,int,long。但是如果整数的范围在[0,100000],那么只需要17bits就足够存储了,因为2^17=131072。但是,你不能够选择short来存储,因为short存储[65536,100000]之间的数会溢出。如果你用int来存储,那么每个数至少要浪费15bits的空间,大约47%的内存空间。

    Lucene/Solr4.0最值得期待一个改进就是内存效率。经过一些基准测试发现,相对Lucene3.x而言,像Solr或者ElasticSearch这类基于Lucene的应用可以减少2/3的内存使用。Lucene中用到的一项技术就是位压缩(bit-packing).这意味着整型数组的类型从固定大小(8,16,32,64)4种类型,扩展到了[1-64]位共64类型。如果用这种方式存储17-bits大小的整型,那么相对int[]而言,会节约47%的内存。

    Bit-packing的接口定义如下:

interface Mutable {


  longget(int index);


  voidset(int index, long value);


  int size();


}


在这个接口下面,有4种不同内存效率和运行效率的实现。

1.    Direct8, Direct16, Direct32 and Direct64 ,只是 byte[], short[],int[],long[]的一个简单包装,


2.    Packed64, 以块连续的方式把数据存储到 64-bits (long类型) 大小的内存块里面。一个数值可能会跨两个内存块。


3.    Packed64SingleBLock,看起来像 Packed64,但实际上利用位填充来做代替跨多个块的数据存储。 (每个值最大占用32 bits),


4.    Packed8ThreeBlocks Packed16ThreeBlocks, 3 bytes (每个值24 bits) or 3 shorts (每个值48 bits)来存储数据。


具体的实现可以参看Lucene的源代码。

Direct{8,16,32,64}

这些类的方法直接把操作映射到对应的数组里:

§  Direct8: byte[],


§  Direct16: short[],


§  Direct32: int[],


§  Direct64: long[].


这些类的执行效率会非常高,但是缺点也是相当明显。如果要存储17-bits的数,就需要用Direct32,内存依然浪费了47%

 

 

Packed64

         这种实现方式把数值依次存储到64-bits 块中。这种实现方式是最紧凑的,如果要存储100万的17-bits数值,就只需要17*1000000/8 ~= 2MB内存空间。唯一的一个缺陷就是一些数值会跨越两个不同的Blocks。为了提高程序运行效率,在编码实现上就需要一些技巧,比如以不同的偏移量和掩码值更新两个Block.

Packed64SingleBlock

这种实现和Packed64相似,但是一个数值不会跨越多个Block。例如:如果要存储21-bits数值,那么一个64-bits Block只能存储3个数值,剩下的1bit就浪费了(大约有2%的空间损失)。下表是类中已经实现的N-bits数值存储表

Bits per value


Values per  block


Padding bits


Space loss


32


2


0


0%


21


3


1


2%


16


4


0


0%


12


5


4


6%


10


6


4


6%


9


7


1


2%


8


8


0


0%


7


9


1


2%


6


10


4


6%


5


12


4


6%


4


16


0


0%


3


21


1


2%


2


32


0


0%


1


64


0


0%


Packed{8,16}ThreeBlocks

这种类型的接口实现是用3-bytes或者3-shorts来存储一个数值。所以,它们只适用于24-bits 或者 48-bits数值。但是其最大值是Integer.MAX_VALUE/3,所以保存这些数值的数组用一个int寻址就可以了。

 

性能比较

byte,short,int.,long这些原生的存储方式相比,内存空间利用率有非常大的提高,但是执行时间呢?直觉告诉我们会有一定的差距,但是差距有多大呢?Lucene和核心开发者Adrien Grand经过测试,得出的结论如下:Direct*接口实现比Packed643倍,比如Packed64SingleBlock2倍,然而,有趣的是Packed*ThreeBlocks接口实现几乎跟Direct*一样快。但是当数据大小只有1bit或者2bits时,由于CPU缓存的缘故,Packed64 and Packed64SingleBLock运行效率快很多。

这些实现方式的读写效率如何呢?Lucene 核心开发者 Daniel Lemire作了一个测试,用C++实现的程序,用GNU GCC4.6.2 compiler对程序进行了优化。程序运行在macbook air(Inter core i7)上。他得出了如下的结论:一般而言,bit宽度越小,unpacking(读操作)越快。Packing(写操作)速率在bit-witdh=8或者bit-width=16时会更快一些。如下图:

wKioL1PWFI-ztwW0AADZWr6h0zU389.jpg

尽管bit-packing技术可以明显地减少程序中的内存的使用,但是很少有人在程序中使用。这是因为:1、大多数应用中,开发者并不知道每个数值究竟需要多少bits28163264 bits的数据类型是编程语言内置的,但是bit-packing需要额外的编码处理。但是不管怎样,bit-packing技术在降低内存使用上是十分出色的,特别是读操作上,而且并没有过多地损失性能。

 

前面的内容算是翻译的吧,反正是别人的东西,我只是用我自己的理解或者仅仅是中文转述了一遍。下面的内容则是根据代码领悟出来的东西。以Packed64SingleBlock4为例,研究Lucene中到底是如何实现bit-packing的。

Packed64SingleBlock4是把一个long类型(64-bit block)4-bit为单位,分成了16个格子。每个格子可以存储的最大值是15. 其实现的代码如下:

wKioL1PWFJqRtuhmAAIj2-SX524135.jpg

存储的规则是:低对低,高对高:

 开始存储4-bit[0]=15时,blocks[0]=0x000000000000000f=15

    然后存储4-bit[1]=15时,blocks[0]= 0x00000000000000ff=255

    然后存储4-bit[2]=15时,blocks[0]= 0x0000000000000fff=4095

    ……

然后存储4-bit[15]=15时,blocks[0]=0xffffffffffffffff=-1

然后存储4-bit[16]=15时,blocks[1]= 0x000000000000000f=15

即下标小的在long的低位,下标大的排列的long的高位。

         注释后的代码如下:

    @Override

   publiclongget(int index) {

   //定位到blocks中相应的64-bits槽中

   //notice: index >>> 4 = index/64

      finalint o = index >>> 4; 

    //定位到64-bits槽相应的格子中,一个64-bits槽只有16个格子

      finalint b = index & 15;    

    //确定格子的偏移位置,(一个格子要占4-bits)

     //notice: b<< 2 = b * 4

      finalint shift = b << 2;    

      //通过   blocks[o] >>> shift 去除低位的内容

      //通过 &15L 去除高位的内容,这样就正好取得4-bits格子里面的数值

      return(blocks[o] >>> shift) & 15L;

    }


   @Override

    publicvoidset(int index, longvalue) {

   //get方法相同

      finalint o = index >>> 4;

      finalint b = index & 15;

      finalint shift = b << 2;

    /*

     * 15L << shift 表示把清空滑窗移动到对应的格子位置

     * blocks[o] & ~(15L << shift) 其实是两步:第一步 ~(15L << shift) 得到的结果就是 000011…1;第二步 blocks[o] & ~(15L << shift)就正好把4-bits格子清空

     * value<< shift 表示把数值滑窗移动到对应的格子位置,

由于前面已经清空的格子的内容, (blocks[o] & ~(15L << shift)) | (value << shift) 操作就正好可以把数值写入到格子中

     * */

      blocks[o] = (blocks[o] & ~(15L << shift)) | (value << shift);


    }        

代码关键的地方在于位操作,如果对位操作不了解,那么理解起来就会很困难。 

参考博客:

http://blog.jpountz.net/post/25530978824/how-fast-is-bit-packing

http://lemire.me/blog/archives/2012/03/06/how-fast-is-bit-packing/