Lucene源代码学习之 PackedInts
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*接口实现比Packed64快3倍,比如Packed64SingleBlock快2倍,然而,有趣的是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时会更快一些。如下图:
尽管bit-packing技术可以明显地减少程序中的内存的使用,但是很少有人在程序中使用。这是因为:1、大多数应用中,开发者并不知道每个数值究竟需要多少bits。2、8,16,32,64 bits的数据类型是编程语言内置的,但是bit-packing需要额外的编码处理。但是不管怎样,bit-packing技术在降低内存使用上是十分出色的,特别是读操作上,而且并没有过多地损失性能。
前面的内容算是翻译的吧,反正是别人的东西,我只是用我自己的理解或者仅仅是中文转述了一遍。下面的内容则是根据代码领悟出来的东西。以Packed64SingleBlock4为例,研究Lucene中到底是如何实现bit-packing的。
Packed64SingleBlock4是把一个long类型(64-bit block)以4-bit为单位,分成了16个格子。每个格子可以存储的最大值是15. 其实现的代码如下:
存储的规则是:低对低,高对高:
开始存储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/
转载于:https://blog.51cto.com/sbp810050504/1531624
Lucene源代码学习之 PackedInts相关推荐
- lucene源代码学习之 lucene的经典打分过程
Lucene中默认的打分模型是VSM(Vector Space Model),其打分公式如下: 看到很多文章都是对这个公式进行解析,但问题的关键在于看了一大段的解析之后,依然不懂其中的细节.我们直接从 ...
- php7.0 cli,PHP-7.1 源代码学习:php-cli 启动流程
前言 php cli main 函数 configure & make 默认构建目标为 php-cli,相关代码在 sapi/cli 目录下,php_cli.c 文件中能够找到 main(入口 ...
- eos 源代码学习笔记一
文章目录 eos 源代码学习笔记 1.eos 中的常见合约类型 2.语言环境局部( locale )变量的使用简介(目的是通过 gettext 软件包 来实现软件的全球化) 3.eos 源代码的一些优 ...
- BT源代码学习心得(十):客户端源代码分析(相关对象一览) -- 转贴自 wolfenstein (NeverSayNever)
BT源代码学习心得(十):客户端源代码分析(相关对象一览) Author:wolfenstein(NeverSayNever), BitTorrent/download.py中的Multitorren ...
- BT源代码学习心得(一):总体描述 -- 转贴自wolfenstein (NeverSayNever)
BT源代码学习心得(一):总体描述 发信人: wolfenstein (NeverSayNever), 个人文集 标 题: BT源代码学习心得(一):总体描述 发信站: 水木社区 (Fri Jul ...
- BT源代码学习心得(七):跟踪服务器(Tracker)的代码分析(HTTP协议处理对象) -- 转贴自 wolfenstein (NeverSayNever)
BT源代码学习心得(七):跟踪服务器(Tracker)的代码分析(HTTP协议处理对象) author: wolfenstein (NeverSayNever) 上次我们分析了Tracker类初始化的 ...
- BT源代码学习心得(二):程序运行参数的获取 -- 转贴自 wolfenstein (NeverSayNever)
BT源代码学习心得(二):程序运行参数的获取 发信人: wolfenstein (NeverSayNever), 个人文集 标 题: BT源代码学习心得(二):程序运行参数的获取 发信站: 水木社区 ...
- BT源代码学习心得(四):种子文件的生成 -- 转贴自wolfenstein (NeverSayNever)
BT源代码学习心得(四):种子文件的生成 author: wolfenstein 在知道种子文件采取的编码方式后,我们现在可以来看一个种子文件具体是如何生成的了.在BT中,生成种子文件的可执行模块是 ...
- BT源代码学习心得(三):种子文件的编码方式 -- 转贴自wolfenstein (NeverSayNever)
BT源代码学习心得(三):种子文件的编码方式 author: wolfenstein BT的作者使用了一种比较简单易懂的编码方式来对设计种子文件.这种编码方式能够很简单得对python中的各种数据类型 ...
最新文章
- API Hooking 的原理
- VC++把输入的字符转换为十六进制
- 【opencv】5.cv::findContours和cv::drawContours()
- C# 操作XML入门
- python和matlab交互_MATLAB调用python,交互
- centos6安装mysql并远程连接_如何开启phpstudy中mysql的远程连接
- nginx的脚本引擎(一)
- 2.2_ 4_ FCFS、SJF、 HRRN调度算法
- knn机器学习算法_K-最近邻居(KNN)算法| 机器学习
- 学python电脑硬件_Python实现的读取电脑硬件信息功能示例
- 35岁-59岁-人生的二个世界
- CentOS上安装多版本Python问题
- java 获取excel的行数_关于Java:如何使用POI库获取Excel文件中的行数?
- 4、智慧变电站 - 外围电塔及电线绘制
- cs224w(图机器学习)2021冬季课程学习笔记15 Frequent Subgraph Mining with GNNs
- python 多因素方差分析_SPSS分析技术:多元方差分析
- SQL中where in的用法
- Lua:协程,coroutine.create,coroutine.resume, coroutine.yield
- 【转】Mac 下钥匙串不能授权访问怎么解决--不错
- Java中如何不用中间变量来使两个变量交换值