如何使用字节序列化双精度数组(二进制增量编码,用于低差单调浮点数据集)...
低延遲系統需要高性能的消息處理和傳遞。 由于在大多數情況下,數據必須通過有線傳輸或進行序列化才能保持持久性,因此編碼和解碼消息已成為處理管道的重要組成部分。 高性能數據編碼的最佳結果通常涉及應用程序數據細節的知識。 本文介紹的技術是一個很好的例子,說明了如何利用數據的某些方面在延遲和空間復雜度方面使編碼受益。
通常,高頻交易系統周圍傳遞的最大消息以訂單簿摘錄的形式表示某種交換狀態。 即,典型的市場數據(報價)消息包含標識工具的信息和代表訂單簿頂部的一組值。 數據集的強制性成員是以實數表示的價格/匯率信息(用于買賣雙方)。
從優化的角度來看,使該數據集非常有趣的原因是:
因此,在實踐中,我們處理的是浮點數集,這些浮點數不僅經過排序(升序出價和降價要求),而且相鄰值彼此相對“接近”。 請記住,消息處理延遲對于大多數交易系統而言至關重要,因此市場行情波動,因為關鍵實體之一需要盡可能高效地傳遞。
讓我演示如何利用所有這些功能將數字數據編碼為非常緊湊的二進制形式。 首先,兩個預告情節說明了不同序列化方法之間的區別:
到目前為止,我們可以看到,與基于標準ByteBuffer的序列化相比,增量編碼在編碼時間上可獲得更好的結果,但是一旦數組長度超過40,它就會比Kryo (最流行的高性能序列化庫之一)更差。不過,在這里重要的是一個典型的用例場景,對于高頻交易,市場消息恰好適合陣列長度的10-40
當我們檢查生成的消息大小(作為數組長度的函數)時,它會變得更加有趣:
此時,應用增量編碼的好處將變得顯而易見(藍色曲線同樣適用于字節緩沖區和Kryo??序列化)。 這里發生的是,在掌握了一些有關要處理的數據的特定知識之后,可以安全地做出一些假設,從而導致序列化占用更多的CPU資源,但是在空間方面卻效率高得多。 增量壓縮背后的想法非常簡單。 讓我們從一個整數數組開始:
- [85103、85111、85122、85129、85142、85144、85150、85165、85177]
如果這些是整數,則不進行任何壓縮就必須使用4 * 9 = 36字節來存儲數據。 在這組數字中,特別有趣的是它們彼此相對緊密地聚集在一起。 我們可以通過引用第一個值輕松地減少存儲數據所需的字節數,然后生成一個對應的增量數組:
- 參考:85103,[8、19、26、39、41、47、62、74]
哇! 現在我們可以縮小為字節數組。 讓我們再次進行計算。 我們需要4個字節作為參考值(仍為int),每個增量= 8 * 1個字節= 12個字節。
與原始的36個字節相比,這是一個很大的改進,但仍有一些優化的余地。 與其從參考值計算增量,不如依次存儲每個前任的差異:
- 參考:85103,[8、11、7、13、2、6、15、12]
結果是一組具有低方差和標準偏差的非單調數字。 我希望事情已經明確了。 盡管如此,還是值得詳細說明。
至此,我們基本上得出的是一個非常適合二進制編碼的集合。 對于我們的示例,這僅意味著可以在單個字節中容納2個增量。 我們只需要一個半字節(4位)即可容納0-15范圍內的值,因此我們可以輕松地最終將原始數組壓縮為4(供參考)+ 8 * 1/2 = 8個字節。
由于價格是用十進制數表示的,因此應用帶有二進制編碼的增量壓縮將涉及建立最大支持的精度并將小數視為整數(將它們乘以10 ^精度),從而使精度為6的1.12345678成為1123456整數。 到目前為止,所有這些都是純粹的理論推測,本文開頭有一些預告片。 我想這是演示如何使用2個非常簡單的類在Java中實現這些想法的恰當時機。
我們將從編碼方面開始:
package eu.codearte.encoder;import java.nio.ByteBuffer;import static eu.codearte.encoder.Constants.*;public class BinaryDeltaEncoder {public static final int MAX_LENGTH_MASK = ~(1 << (LENGTH_BITS - 1));public ByteBuffer buffer;public double[] doubles;public int[] deltas;public int deltaSize, multiplier, idx;public void encode(final double[] values, final int[] temp, final int precision, final ByteBuffer buf) {if (precision >= 1 << PRECISION_BITS) throw new IllegalArgumentException();if ((values.length & MAX_LENGTH_MASK) != values.length) throw new IllegalArgumentException();doubles = values; deltas = temp; buffer = buf;multiplier = Utils.pow(10, precision);calculateDeltaVector();if (deltaSize > DELTA_SIZE_BITS) throw new IllegalArgumentException();buffer.putLong((long) precision << (LENGTH_BITS + DELTA_SIZE_BITS) | (long) deltaSize << LENGTH_BITS | values.length);buffer.putLong(roundAndPromote(values[0]));idx = 1;encodeDeltas();}private void calculateDeltaVector() {long maxDelta = 0, currentValue = roundAndPromote(doubles[0]);for (int i = 1; i < doubles.length; i++) {deltas[i] = (int) (-currentValue + (currentValue = roundAndPromote(doubles[i])));if (deltas[i] > maxDelta) maxDelta = deltas[i];}deltaSize = Long.SIZE - Long.numberOfLeadingZeros(maxDelta);}private void encodeDeltas() {if (idx >= doubles.length) return;final int remainingBits = (doubles.length - idx) * deltaSize;if (remainingBits >= Long.SIZE || deltaSize > Integer.SIZE) buffer.putLong(encodeBits(Long.SIZE));else if (remainingBits >= Integer.SIZE || deltaSize > Short.SIZE) buffer.putInt((int) encodeBits(Integer.SIZE));else if (remainingBits >= Short.SIZE || deltaSize > Byte.SIZE) buffer.putShort((short) encodeBits(Short.SIZE));else buffer.put((byte) encodeBits(Byte.SIZE));encodeDeltas();}private long encodeBits(final int typeSize) {long bits = 0L;for (int pos = typeSize - deltaSize; pos >= 0 && idx < deltas.length; pos -= deltaSize)bits |= (long) deltas[idx++] << pos;return bits;}private long roundAndPromote(final double value) {return (long) (value * multiplier + .5d);} }在詳細介紹之前,請先介紹幾句話。 該代碼不是完整的,成熟的解決方案。 它的唯一目的是演示提高應用程序序列化協議的某些位的難易程度。 由于它受到了微基準測試,它也不會引起gc壓力,因為即使是最快的次要gc的影響也會嚴重扭曲最終結果,從而使api變得丑陋。 該實現也是次優的,尤其是在CPU方面,但是證明微優化不是本文的目標。 話雖如此,讓我們看看它的作用(大括號中的行號)。
編碼方法首先進行一些基本的健全性檢查{17,18},計算將十進制轉換為整數{20}所用的乘數,并計算calculateDeltaVector()。 這又有兩個影響。
然后,encode()方法存儲正確反序列化所需的一些元數據。 它以位為單位打包精度,增量大小,并以前64位{24}打包數組長度。 然后,它存儲參考值{25}并啟動二進制編碼{27}。
編碼增量執行以下操作:
可能需要詳細說明的最后一點是encodeBits()方法本身。 根據在參數中傳遞的類型大小(以位為單位),它會循環一個臨時long,其唯一目的是用作位集,并寫入代表從long值的最高有效部分到最低有效部分的連續增量的位(范圍為字號)。
正如預期的那樣,解碼與編碼完全相反,并且主要是關于使用元數據來重建原始double數組,直至達到指定的精度:
package eu.codearte.encoder;import java.nio.ByteBuffer;import static eu.codearte.encoder.Constants.DELTA_SIZE_BITS; import static eu.codearte.encoder.Constants.LENGTH_BITS;public class BinaryDeltaDecoder {private ByteBuffer buffer;private double[] doubles;private long current;private double divisor;private int deltaSize, length, mask;public void decode(final ByteBuffer buffer, final double[] doubles) {this.buffer = buffer; this.doubles = doubles;final long bits = this.buffer.getLong();divisor = Math.pow(10, bits >>> (LENGTH_BITS + DELTA_SIZE_BITS));deltaSize = (int) (bits >>> LENGTH_BITS) & 0x3FFFFFF;length = (int) (bits & 0xFFFFFFFF);doubles[0] = (current = this.buffer.getLong()) / divisor;mask = (1 << deltaSize) - 1;decodeDeltas(1);}private void decodeDeltas(final int idx) {if (idx == length) return;final int remainingBits = (length - idx) * deltaSize;if (remainingBits >= Long.SIZE) decodeBits(idx, buffer.getLong(), Long.SIZE);else if (remainingBits >= Integer.SIZE) decodeBits(idx, buffer.getInt(), Integer.SIZE);else if (remainingBits >= Short.SIZE) decodeBits(idx, buffer.getShort(), Short.SIZE);else decodeBits(idx, buffer.get(), Byte.SIZE);}private void decodeBits(int idx, final long bits, final int typeSize) {for (int offset = typeSize - deltaSize; offset >= 0 && idx < length; offset -= deltaSize)doubles[idx++] = (current += ((bits >>> offset) & mask)) / divisor;decodeDeltas(idx);}} 帶有一些測試類的源代碼可以在這里找到。 請記住,即使事實證明該代碼可以正常工作,也不適合生產。 您絕對可以使它工作而無需臨時陣列,用聰明的方法代替最大陣列大小時,可以代替全陣列掃描,也可以通過采用倒數近似除法而無需進行重量級除法。 隨意選擇這些提示或進行不同的微優化,并構建自己的專有增量編碼協議。 對于對延遲敏感的交易應用程序而言,它具有很大的區別,可將液體工具的市場數據消息大小減小20-30倍。 當然,如果切換到增量壓縮二進制編碼會對您的應用程序生態系統帶來任何價值,那么您必須弄清楚自己。 隨便發表您的發現與評論!
翻譯自: https://www.javacodegeeks.com/2014/01/how-to-serialize-an-array-of-doubles-with-a-byte-binary-delta-encoding-for-low-varianced-monotonic-sets-of-floating-point-data.html
總結
以上是生活随笔為你收集整理的如何使用字节序列化双精度数组(二进制增量编码,用于低差单调浮点数据集)...的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 土建zdm快捷键(ZDM快捷键)
- 下一篇: 实现自定义的未来