在FST算法实现的过程中,存在一些代码优化。本文涉及到基础部分内容需要看 1. 图解FST构造算法。
写入顺序
先顺序写入,然后将数据反转
在1. 图解FST构造算法中的图表中,可以看到类似的下图:字节数组中,label在flags后。之所以flags需要在大端,是为了方便搜索算法的实现,搜索算法的实现后续会单独写一篇。
但实际上,FST的实现并不是按照下面的顺序写入,而是按这个顺序的反序写入。
- 下一个节点的Index
- Output
- Label
- Flags
FST的实现里面,先按照下面列表的顺序进行写入
- Flags
- Label
- Output
- 下一个节点的Index
当一个Node完成冻结/写入字节数组,然后对数组进行反转,就能获取到图解算法中顺序。
因此,假如你是按小端序写入一段数据,读取出来时就需要使用大端序了,哈哈哈。
多Arc冻结/存储优化
如果一个Node中存在多个Arc需要冻结的情况,在工程上其实进行一些优化,优化的方向可以分为2种
- 节省存储空间
- 优化搜索效率
Flag
这里新增2个flag。
Flag | Value | Description |
---|---|---|
ARCS_FOR_BINARY_SEARCH | 32 | 表示使用直接寻址(DirectAddressing) |
ARCS_FOR_DIRECT_ADDRESSING | 64 | 标识二分查找(BinarySearch) |
直接寻址/(DirectAddressing)
本质:使用比特位减少label的占用空间
我们将写入内存的label都移除掉,获得下面的效果
在代码实现中,因为移除了label后,需要考虑分段的问题,即我怎么知道哪一段数据跟某个Arc是对应的?有一个简单的做法,即我们把分段的大小进行固定,这样我们就能获取到对应的Arc的数据了。在这里,分段的最大长度为3,那我们就记录下Arc的没有Label时的最大长度为3。
我们可以知道,在ASCII码中,A<B<C<D。这个时候,我们就可以仅保存A,然后B、C、D的对应的Arc的Label之需要保存一个差值即可。
Label | 差值 |
---|---|
A | 0 |
B | 1 |
C | 2 |
D | 3 |
在FST的构造算法中,Arc的顺序一定是从小到大的,这样,我们就可以利用比特位来保存这个差值数据。
因为Arc=A的差值不需要记录,所以我们只需要记录B、C、D的差值即可。上图中,字节从右侧起,第一个1表示差值为B和A的差值为1,第二个表示C和A差值为2,以此类推。
这里有一个设计非常巧妙的地方,这个比特位既标记了Label的差值,而比特位为1的数量表示了这一组Arc的数量。
这里我贴一段比特位长度计算的代码,用于计算当前存储差值的数组的长度(根据需要的位数来)
go
// Gets the number of bytes required to flag the presence of each arc in the given label range, one bit per arc.
// 获取标记给定标签范围中每个弧的存在所需的字节数,每个弧一位。
func getNumPresenceBytes(labelRange int) int {
// 可以看作labelRange/8 + (labelRange%8>8)? 1: 0
return (labelRange + 7) >> 3
}
然后,我们给这个Node标记flags=64,标记这段数据是使用直接寻址(DirectAddressing)的方式进行数据的记录的。
二分查找/(BinarySearch)
本质:调整Arc的写入顺序,优化搜索效率
由于Output导致写入字节数组的长度不一致,如上图所示,黄色区块的长度为5,而其他区块长度为4。
二分查找算法中,需要保证每个Arc的字节长度保持一致。
增加一个Arc数量标记,标识当前的Arc的数量为4(index=45)。
增加一个类型标记,标识当前为二分查找(BinarySearch)进行存储Arc数据(index=32)。
如何判断使用那种写入优化
是否使用优化策略
跟Node的深度和Arcs的数量相关,这里是源码中对应的优化的策略
java
private boolean shouldExpandNodeWithFixedLengthArcs(Builder<T> builder, Builder.UnCompiledNode<T> node) {
return builder.allowFixedLengthArcs &&
((node.depth <= FIXED_LENGTH_ARC_SHALLOW_DEPTH && node.numArcs >= FIXED_LENGTH_ARC_SHALLOW_NUM_ARCS) ||
node.numArcs >= FIXED_LENGTH_ARC_DEEP_NUM_ARCS);
}
使用哪种优化策略
信用这个我不是很理解什么意思,从代码逻辑上看,还是优先考虑空间的消耗。
java
private boolean shouldExpandNodeWithDirectAddressing(Builder<T> builder, Builder.UnCompiledNode<T> nodeIn,
int numBytesPerArc, int maxBytesPerArcWithoutLabel, int labelRange) {
// Anticipate precisely the size of the encodings.
int sizeForBinarySearch = numBytesPerArc * nodeIn.numArcs;
int sizeForDirectAddressing = getNumPresenceBytes(labelRange) + builder.numLabelBytesPerArc[0]
+ maxBytesPerArcWithoutLabel * nodeIn.numArcs;
// Determine the allowed oversize compared to binary search.
// This is defined by a parameter of FST Builder (default 1: no oversize).
int allowedOversize = (int) (sizeForBinarySearch * builder.getDirectAddressingMaxOversizingFactor());
int expansionCost = sizeForDirectAddressing - allowedOversize;
// 如果出现以下情况之一,请选择直接寻址:
// - 直接寻址大小小于二进制搜索。
// 在这种情况下,按减少的大小增加信用额度(以后使用)。
// - 直接寻址大小比二进制搜索大,但正信用允许过大。
// 在这种情况下,将信用额减去oversize。
// 此外,不要试图过大到明显过大的节点大小
//(这是DIRECT_ADDRESSING_MAX_OVERSIZE_WITH_CREDIT_FACTOR参数)。
if (expansionCost <= 0 || (builder.directAddressingExpansionCredit >= expansionCost
&& sizeForDirectAddressing <= allowedOversize * DIRECT_ADDRESSING_MAX_OVERSIZE_WITH_CREDIT_FACTOR)) {
builder.directAddressingExpansionCredit -= expansionCost;
return true;
}
return false;
}
结语
FST的存储优化主要是为了解决空间消耗问题和降低代码实现的复杂程度。
希望对你理解FST的原理有一点点帮助,也欢迎帮忙关注下我的开源项目github.com/geange/luce...