來學習一下DirectMonotonicWriter
類的代碼. 源碼版本: 8.7.0java
先上一下源碼註釋:算法
Write monotonically-increasing sequences of integers. This writer splits data into blocks and then for each block, computes the average slope, the minimum value and only encode the delta from the expected value using a DirectWriter.數組
簡單翻譯:微信
用來寫入單調遞增的int序列. 它把數據分紅塊,而後對於每個塊, 計算平均斜率,最小值,而後只使用
DirectWriter
來編碼給定數字的delta(翻譯成增量,有更好的翻譯再來修改,歡迎建議).markdown
它不是一個通用的解決方案, 只適用於單調遞增數組, 他經過計算元素之的增量, 讓全部元素迅速變小. 以後使用DirectWriter
來進行壓縮存儲,以得到更好的壓縮率. 所以它很適合存儲文件地址之類比較連續的數據.ide
讓咱們經過一個實例, 來知道這個類作了什麼,以後再學習一下具體的代碼.oop
假如要存儲4個數字, {100,102,103,105}.學習
首先算一下平均斜率: avgInc = (105-100) / 3 = 1.6666666
. 以後對每一個數字計算符合斜率的指望值與實際值的差值
, 算法爲: expected = (long) (avgInc * (long) idx)
, 而後使用每一個數字減去指望值,數組變成了 {100,101,100,100}.ui
求出最小值, min = 100
.this
而後,對於每一個位置的數字, 計算它與最小值的差值. 數組變成了: {0,1,0,0 }
算一下maxDelta
, 這個值是上面最後生成的數組中, 最大的一個數字,若是上面的數組所有是0, 這個值也爲0, 就說明給定的原始數組是一個標準的單調遞增的等差數列, 那麼就不用存原始值了,直接用最小值和斜率就能所有算出來. 若是不爲0, 那麼maxDelta
就是用DirectWriter
存儲時的最大值, 不知道爲啥DirectWriter
存儲須要提早告知最大值的能夠看這裏~ lucene中DirectWriter類的源碼學習
用maxDelta計算出須要的最大bit數. bitsPerValue
.
以後進行實際的寫入.
min,avgInc, Offset, bitsPerValue
, 寫入meta文件.咱們寫個單測, 將上面的信息實際寫入文件. 讓咱們看看十六進制的文件.
Meta文件:
圖中:
DirectWriter
中,每個數字只使用1個bit就能夠表示. 所以這裏存儲了1, 只佔用一個字節.這是data文件, 若是瞭解DirectWriter
的話, 能夠知道, 此次寫入是以byte爲單位的. 所以前面的40=0100 0000
, 前面的0100
就是咱們存儲的值, 與上面分析相符合. 後面4位0是byte自動填充的.
然後面三個字節的0, 是DirectWriter
自動寫入的,與咱們這次實驗無關.
根據上面的meta信息及data信息,是徹底能夠推算出原始值的(壓縮了而解壓不了豈不是笑話).
一個單調遞增數組(只討論正數), 鏈接首尾以後, 必然是一條在第一象限的相似於圖中的直線.
我數學很差...
這條直線是y=ax+b
. 咱們記錄下來: b
,也就是min值.a
就是斜率. 記下來這兩個信息就能夠還原出這條直線.
以後咱們有一個數組, 下標能夠帶入公式算出對應下標的指望值,數組具體位置上保存着, 實際值與指望值之間的差值, 再減去最小值
. 就能夠還原每個點了, 也就是原始數據.
上面一不當心說多了, 好多劇透了, 因此源碼部分就簡單看一下.
// 一塊有多少個int, 這裏是 2的shift次方個
public static final int MIN_BLOCK_SHIFT = 2;
public static final int MAX_BLOCK_SHIFT = 22;
// 這個類, 其實不知道是爲了誰寫
// 可是仍然不妨礙一個記錄元數據,一個記錄真正的數據,
// 寫field信息能夠用,其餘的docValue之類的也能夠
final IndexOutput meta;
final IndexOutput data;
// 總數, 不區分chunk,block等等,對於這個類來講,就是你想要我寫多少個。
final long numValues;
// data文件初始化的時候的文件寫入地址.
final long baseDataPointer;
// 內部緩衝區
final long[] buffer;
// 當前已經buffer了多少個
int bufferSize;
// 總數計數,bufferSize會被清除的
long count;
boolean finished;
複製代碼
具體解釋見註釋, 注意一下有個buffer便可.
DirectMonotonicWriter(IndexOutput metaOut, IndexOutput dataOut, long numValues, int blockShift) {
if (blockShift < MIN_BLOCK_SHIFT || blockShift > MAX_BLOCK_SHIFT) {
throw new IllegalArgumentException("blockShift must be in [" + MIN_BLOCK_SHIFT + "-" + MAX_BLOCK_SHIFT + "], got " + blockShift);
}
if (numValues < 0) {
throw new IllegalArgumentException("numValues can't be negative, got " + numValues);
}
// 根據總數,以及每塊的數據,來算總共須要的塊的數量。 算法約等於,總數 / (2 ^ blockShift);
// 這裏只是校驗一下這兩個數字的合法性,實際限制在
final long numBlocks = numValues == 0 ? 0 : ((numValues - 1) >>> blockShift) + 1;
if (numBlocks > ArrayUtil.MAX_ARRAY_LENGTH) {
throw new IllegalArgumentException("blockShift is too low for the provided number of values: blockShift=" + blockShift +
", numValues=" + numValues + ", MAX_ARRAY_LENGTH=" + ArrayUtil.MAX_ARRAY_LENGTH);
}
this.meta = metaOut;
this.data = dataOut;
this.numValues = numValues;
// blockSize算到了, 而後緩衝區的大小就是blockSize或者極限狀況下不多,就是numValues.
final int blockSize = 1 << blockShift;
this.buffer = new long[(int) Math.min(numValues, blockSize)];
this.bufferSize = 0;
this.baseDataPointer = dataOut.getFilePointer();
}
複製代碼
注意buffer大小的計算, 若是數據足夠多, buffer的大小爲: 2 << blockShift. 不然buffer爲numValues.
/** * Write a new value. Note that data might not make it to storage until * {@link #finish()} is called. * * @throws IllegalArgumentException if values don't come in order * 寫一個新的值, * 可是不必定當即存儲,可能在finish的時候才存儲 * 若是傳入的值不是遞增的,就報錯 */
public void add(long v) throws IOException {
// 檢查是不是單調遞增
if (v < previous) {
throw new IllegalArgumentException("Values do not come in order: " + previous + ", " + v);
}
// 內部緩衝區滿,意味着,分塊的一塊滿了, 緩衝區是以前根據分塊大小算好的
if (bufferSize == buffer.length) {
flush();
}
// 緩衝區沒滿,先放到內存buffer裏面
buffer[bufferSize++] = v;
previous = v;
count++;
}
複製代碼
和常見的output同樣,一個樸實無華的內存buffer,若是buffer滿了則調用flush.
注意在add時會檢測當前值是否大於上一個, 來保存傳入數據是單調遞增的.
/** * // 一個塊滿了,或者最終調用finish了,就寫一次 * <br/> * <br/> * <b>計算方法終於搞明白了,存儲一個單調遞增數組,要存儲斜率,最小值,以及delta,再加上index就能夠算出來</b> * 舉例 [100,101,108] 通過計算以後存儲的[3,0,3], 斜率4.0. 最小值97. * 開始計算: * 1. 100 = 97 + 3 + 0 * 4.0 * 2. 101 = 97 + 0 + 1 * 4.0 * 3. 108 = 97 + 3 + 2 * 4.0 * 完美 * <br/> * <br/> * 一個block,這麼搞一下 * * @throws IOException */
private void flush() throws IOException {
assert bufferSize != 0;
// 斜率算法, 最大減去最小除以個數,常見算法
final float avgInc = (float) ((double) (buffer[bufferSize - 1] - buffer[0]) / Math.max(1, bufferSize - 1));
// 根據斜率,算出當前位置上的數字,比按照斜率算出來的數字,多了多少或者小了多少,這就是增量編碼
// 當前存了個3,預期是500,那就存儲-497.
// 有啥意義麼? 能把大數字變成小數字?節省點空間?
// 這裏會把單調遞增的數字,算一條執行出來,首尾鏈接點. 而後每一個數字對着線上對應點的偏移距離,畫個圖會好說不少,一個一元一次方程麼?
for (int i = 0; i < bufferSize; ++i) {
final long expected = (long) (avgInc * (long) i);
buffer[i] -= expected;
}
// 可是存的不是真實值,而是偏移量
long min = buffer[0];
for (int i = 1; i < bufferSize; ++i) {
min = Math.min(buffer[i], min);
}
// 每一個位置上存儲的,不是偏移量了,而是偏移量與最小的值的偏移量
// 而後算個最大偏移量
long maxDelta = 0;
for (int i = 0; i < bufferSize; ++i) {
buffer[i] -= min;
// use | will change nothing when it comes to computing required bits
// but has the benefit of working fine with negative values too
// (in case of overflow)
maxDelta |= buffer[i];
}
// 元數據裏面開始寫, 最小值,平均斜率,data文件從開始到如今寫了多少,
meta.writeLong(min);
meta.writeInt(Float.floatToIntBits(avgInc));
// 當前block, 相對於整個類開始寫的時候, 的偏移量
meta.writeLong(data.getFilePointer() - baseDataPointer);
// 是否是意味着全是0, 也就是絕對的單調遞增,等差數列的意思?
// 若是是等差數列,就不在data裏面寫了,直接在meta裏面記一下最小值就完事了,以後等差就行了
if (maxDelta == 0) {
// 最大偏移量爲,那就寫個0
meta.writeByte((byte) 0);
} else {
// 最大須要多少位
final int bitsRequired = DirectWriter.unsignedBitsRequired(maxDelta);
// 把緩衝的數據實際的寫到data文件去
DirectWriter writer = DirectWriter.getInstance(data, bufferSize, bitsRequired);
for (int i = 0; i < bufferSize; ++i) {
writer.add(buffer[i]);
}
writer.finish();
// 寫一下算出來的最大須要多少位
meta.writeByte((byte) bitsRequired);
}
// 緩衝的數據歸零,這樣就能一直用內存裏的buffer了
bufferSize = 0;
}
複製代碼
每當一個block滿了,或者最終進行flush. 都是以當前的block爲單位:
進行計算最小值,斜率, 及對數組進行轉換.
以後將最小值,斜率, data文件偏移量, 每一個數字須要的bit數量等元數據,寫入對應的元數據文件中.
將按照上面分析的規則, 進行轉換過的數組, 調用DirectWriter
,寫入data文件中.
/** * This must be called exactly once after all values have been {@link #add(long) added}. * 全部數字都被調用過all以後, * 要調用且只能調用一次finish. */
public void finish() throws IOException {
if (count != numValues) {
throw new IllegalStateException("Wrong number of values added, expected: " + numValues + ", got: " + count);
}
// 保證只能調用一次
if (finished) {
throw new IllegalStateException("#finish has been called already");
}
// 調用finish的時候,有緩衝就直接寫,反正也只能調用一次
if (bufferSize > 0) {
flush();
}
finished = true;
}
複製代碼
也是常見的樸實無華, 檢查下相關參數,而後調用一下flush,將最後一點數據寫入磁盤便可.
DirectMonotonicWriter
類, 用來壓縮存儲單調遞增的整數數組. 它會寫入兩個文件, 其中meta
文件存儲計算後的元數據, data
文件存儲轉換後的數組.
他內部進行了分塊, 而後以塊爲單位, 經過計算最小值,斜率等輔助參數, 將原始數據轉換成相對增量,以將大整數轉換成爲小整數. 以後使用DirectWriter
來進行按bit的壓縮存儲. 結合DirectWriter
對小整數壓縮率較高的特色, 這個類實現了對單調遞增數組的高壓縮率的壓縮存儲.
完。
以上皆爲我的所思所得,若有錯誤歡迎評論區指正。
歡迎轉載,煩請署名並保留原文連接。
更多學習筆記見我的博客或關注微信公衆號 <呼延十 >------>呼延十