4千字長文預警!!java
好,再沒有一種序列化方案能像JSON和XML同樣流行,自由、方便,擁有強大的表達力和跨平臺能力。是通用數據傳輸格式的默認首選。不過隨着數據量的增長和性能要求的提高,這種自由與通用帶來的性能問題也不容忽視。python
JSON和XML使用字符串表示全部的數據,對於非字符數據來講,字面量表達會佔用不少額外的存儲空間,而且會嚴重受到數值大小和精度的影響。 一個32位浮點數 1234.5678 在內存中佔用 4 bytes 空間,若是存儲爲 utf8 ,則須要佔用 9 bytes空間,在JS這樣使用utf16表達字符串的環境中,須要佔用 18 bytes空間。 使用正則表達式進行數據解析,在面對非字符數據時顯得十分低效,不只要耗費大量的運算解析數據結構,還要將字面量轉換成對應的數據類型。android
在面對海量數據時,這種格式自己就可以成爲整個系統的IO與計算瓶頸,甚至直接overflow。c++
衆多的序列化方案中,按照存儲方案,可分爲字符串存儲和二進制存儲,字符串存儲是可讀的,可是因爲以上問題,這裏只考慮二進制存儲。二進制存儲中可分爲須要IDL和不須要IDL,或分爲自描述與非自描述(反序列化是否須要IDL)。git
protocol buffersgithub
flat buffersweb
avro正則表達式
Thrift編程
DIMBINjson
使用數值類型而非字面量來保存數值,自己就能節約一筆十分可觀的空間。 protocol buffer爲了實現更高的壓縮率,使用varint去壓縮數值。(不過下面的測試代表,可使用gzip的環境中,這種方案沒有幫助)
二進制格式用經過特定位置來記錄數據結構以及每一個節點數據的偏移量,省去了從字符串中解析數據結構所耗費的時間,避免了長字符串帶來的性能問題,在GC語言中,也大大減小了中間垃圾的產生。
在能夠進行內存直接操做的環境中(包括JS),還能夠經過內存偏移量直接讀取數據,而避免進行復制操做,也避免開闢額外的內存空間。DIMBIN和flatbuffers都使用了這種理念來優化數據存儲性能。在JS環境中,經過創建DataView或者TypedArray來從內存段中提取數據的耗時基本上能夠忽略不計。
二進制方案中存儲字符串須要額外的邏輯進行UTF8編解碼,性能和體積不如JSON這樣的字符串格式。
咱們的數據可視化場景中常常涉及百萬甚至千萬條數據的實時更新,爲解決JSON的性能問題,咱們使用內存偏移量操做的思路,開發了DIMBIN做爲序列化方案,並基於其上設計了許多針對web端數據處理的傳輸格式。
做爲一種簡單直白的優化思路,DIMBIN已經成爲咱們數據傳輸的標準方案,保持絕對的精簡與高效。
咱們剛剛將DIMBIN開源,貢獻給社區,但願能爲你們帶來一個比 JSON/protocol/flatbuffers 更輕、更快、對Web更友好的解決方案。
針對Web/JS環境中的使用,咱們選擇 JSON、protocol buffers、flat buffers、DIMBIN 四種方案,從七個方面進行對比。
Protocolbuffers 和 flatbuffers 表明Google所倡導的完整的workflow。嚴格、規範、統1、面向IDL,爲多端協做所設計,針對python/java/c++。經過IDL生成代碼,多平臺/多語言使用一致的開發流程。若是團隊採用這種工做流,那麼這種方案更便於管理,多端協做和接口更迭都更可控。
可是若是離開了這套工程結構,則顯得相對繁雜。
JSON/XML 和 DIMBIN 是中立的,不須要IDL,不對工程化方案和技術選型做假設或限制。能夠只經過文檔規範接口,也能夠自行添加schema約束。
Protocolbuffers 和 flatbuffers 須在項目設計的早期階段加入,並做爲工做流中的關鍵環節。若是出於性能優化目的而加入,會對項目架構形成較大影響。
JSON基本是全部平臺的基礎設施,無部署成本。
DIMBIN只須要安裝一個軟件包,可是須要數據結構扁平化,若是數據結構沒法被扁平化,將沒法從中受益。
在JS中使用時:
Protocol官網聲稱性能高於JSON,該測試數據顯然不是JS端的,咱們的測試代表其JS端性相對於JSON更差(數據量大的時候差的多)。
全部的二進制方案處理字符串的過程都是相似的:須要將js中的utf16先解碼成unicode,再編碼成utf8,寫入buffer,並記錄每一個字符串的數據地址。該過程性能消耗較大,並且若是不使用varint(protocol buffers)的話,體積也沒有任何優點。
在處理字符串數據時,JSON的性能老是最好的,序列化性能 JSON > DIMBIN > flatbuffers > proto,反序列化 JSON > proto > DIMBIN > flatbuffers
處理數值數據時 Flatbuffers 和 DIMBIN 性能優點明顯,
對於扁平化數值數據的序列化性能 DIMBIN > flatbuffers > JSON > proto,
反序列化 DIMBIN > flatbuffers >十萬倍> JSON > proto
使用字符串與數值混合結構或者純數值時,protocol < DIMBIN < flat < JSON 使用純字符串時,JSON最小,二進制方案都比較大
Gzip以後,DIMBIN和flat的體積最小且基本一致,protocol反而沒有優點,猜想多是varint的反作用。
Protocol 爲強類型語言而設計,所支持的類型比JSON要豐富的多,數據結構也能夠十分複雜; Flatbuffers 支持 數值/布爾值/字符串 三種基本類型,結構與JSON相似; DIMBIN 支持 數值/布爾值/字符串 三種基本類型,目前只支持多維數組的結構(暫不支持也不鼓勵使用鍵值對),更復雜的結構須要在其上封裝。
JSON和DIMBIN都是自描述的,(弱類型語言中)不須要schema,用戶能夠動態生成數據結構和數據類型,生產方和消費方之間約定好便可,若是須要類型檢查則須要在上層封裝。
Protocolbuffers 和 flatbuffers 必須在編碼前先寫好IDL並生成對應的代碼,接口修改則需修改IDL並從新生成代碼、部署到生產端和消費端、再基於其上進行編碼。
Protocolbuffers 和 flatbuffers 服務端與客戶端語言支持都很是完整。二者優先針對C++/Java(android)/Python開發,JS端缺乏一部分高級功能,無完整文檔,須要本身研究example和生成的代碼,不過代碼不長,註釋覆蓋完整。
JSON基本上全部編程語言都有對應的工具。
DIMBIN針對JS/TS開發和優化,目前提供c#版本,c++、wasm、java和python的支持在計劃中。
咱們生成一份典型的數據,使用扁平化和非扁平化兩種結構,使用JSON、DIMBIN、protocol和flat buffers來實現相同的功能,對比各類方案的性能、體積以及便捷程度。
咱們生成兩個版本的測試數據:非扁平化(多層鍵值對結構)數據和等效的扁平化(多維數組)數據
考慮到字符串處理的特殊性,在測試時咱們分開測試了 字符串/數值混合數據、純字符串數據,和純數值數據
// 非扁平化數據 export const data = { items: [ { position: [0, 0, 0], index: 0, info: { a: 'text text text...', b: 10.12, }, }, // * 200,000 個 ], } // 等效的扁平化數據 export const flattedData = { positions: [0, 0, 0, 0, 0, 1, ...], indices: [0, 1, ...], info_a: ['text text text', 'text', ...], info_b: [10.12, 12.04, ...], }
const jsonSerialize = () => { return JSON.stringify(data) }
const jsonParse = str => { const _data = JSON.parse(str) let _read = null // 因爲flat buffers的讀取操做是延後的,所以這裏須要主動讀取數據來保證測試的公平性 const len = _data.items.length for (let i = 0; i < len; i++) { const item = _data.items[i] _read = item.info.a _read = item.info.b _read = item.index _read = item.position } }
import DIMBIN from 'src/dimbin' const dimbinSerialize = () => { return DIMBIN.serialize([ new Float32Array(flattedData.positions), new Int32Array(flattedData.indices), DIMBIN.stringsSerialize(flattedData.info_a), new Float32Array(flattedData.info_b), ]) }
const dimbinParse = buffer => { const dim = DIMBIN.parse(buffer) const result = { positions: dim[0], indices: dim[1], info_a: DIMBIN.stringsParse(dim[2]), info_b: dim[3], } }
DIMBIN目前僅支持多維數組,不能處理樹狀數據結構,這裏不作對比。
首先須要按照proto3語法編寫schema
syntax = "proto3"; message Info { string a = 1; float b = 2; } message Item { repeated float position = 1; int32 index = 2; Info info = 3; } message Data { repeated Item items = 1; } message FlattedData { repeated float positions = 1; repeated int32 indices = 2; repeated string info_a = 3; repeated float info_b = 4; }
使用 protoc 編譯器將schema編譯成JS模塊
./lib/protoc-3.8.0-osx-x86_64/bin/protoc ./src/data.proto --js_out=import_style=commonjs,,binary:./src/generated
// 引入編譯好的JS模塊 const messages = require('src/generated/src/data_pb.js') const protoSerialize = () => { // 頂層節點 const pbData = new messages.Data() data.items.forEach(item => { // 節點 const pbInfo = new messages.Info() // 節點寫入數據 pbInfo.setA(item.info.a) pbInfo.setB(item.info.b) // 子級節點 const pbItem = new messages.Item() pbItem.setInfo(pbInfo) pbItem.setIndex(item.index) pbItem.setPositionList(item.position) pbData.addItems(pbItem) }) // 序列化 const buffer = pbData.serializeBinary() return buffer // 扁平化方案: // const pbData = new messages.FlattedData() // pbData.setPositionsList(flattedData.positions) // pbData.setIndicesList(flattedData.indices) // pbData.setInfoAList(flattedData.info_a) // pbData.setInfoBList(flattedData.info_b) // const buffer = pbData.serializeBinary() // return buffer }
// 引入編譯好的JS模塊 const messages = require('src/generated/src/data_pb.js') const protoParse = buffer => { const _data = messages.Data.deserializeBinary(buffer) let _read = null const items = _data.getItemsList() for (let i = 0; i < items.length; i++) { const item = items[i] const info = item.getInfo() _read = info.getA() _read = info.getB() _read = item.getIndex() _read = item.getPositionList() } // 扁平化方案: // const _data = messages.FlattedData.deserializeBinary(buffer) // // 讀數據(避免延遲讀取帶來的標定偏差) // let _read = null // _read = _data.getPositionsList() // _read = _data.getIndicesList() // _read = _data.getInfoAList() // _read = _data.getInfoBList() }
首先須要按照proto3語法編寫schema
table Info { a: string; b: float; } table Item { position: [float]; index: int; info: Info; } table Data { items: [Item]; } table FlattedData { positions:[float]; indices:[int]; info_a:[string]; info_b:[float]; }
./lib/flatbuffers-1.11.0/flatc -o ./src/generated/ --js --binary ./src/data.fbs
// 首先引入基礎庫 const flatbuffers = require('flatbuffers').flatbuffers // 而後引入編譯出的JS模塊 const tables = require('src/generated/data_generated.js') const flatbufferSerialize = () => { const builder = new flatbuffers.Builder(0) const items = [] data.items.forEach(item => { let a = null // 字符串處理 if (item.info.a) { a = builder.createString(item.info.a) } // 開始操做 info 節點 tables.Info.startInfo(builder) // 添加數值 item.info.a && tables.Info.addA(builder, a) tables.Info.addB(builder, item.info.b) // 完成操做info節點 const fbInfo = tables.Info.endInfo(builder) // 數組處理 let position = null if (item.position) { position = tables.Item.createPositionVector(builder, item.position) } // 開始操做item節點 tables.Item.startItem(builder) // 寫入數據 item.position && tables.Item.addPosition(builder, position) item.index && tables.Item.addIndex(builder, item.index) tables.Item.addInfo(builder, fbInfo) // 完成info節點 const fbItem = tables.Item.endItem(builder) items.push(fbItem) }) // 數組處理 const pbItems = tables.Data.createItemsVector(builder, items) // 開始操做data節點 tables.Data.startData(builder) // 寫入數據 tables.Data.addItems(builder, pbItems) // 完成操做 const fbData = tables.Data.endData(builder) // 完成全部操做 builder.finish(fbData) // 輸出 // @NOTE 這個buffer是有偏移量的 // return builder.asUint8Array().buffer return builder.asUint8Array().slice().buffer // 扁平化方案: // const builder = new flatbuffers.Builder(0) // const pbPositions = tables.FlattedData.createPositionsVector(builder, flattedData.positions) // const pbIndices = tables.FlattedData.createIndicesVector(builder, flattedData.indices) // const pbInfoB = tables.FlattedData.createInfoBVector(builder, flattedData.info_b) // const infoAs = [] // for (let i = 0; i < flattedData.info_a.length; i++) { // const str = flattedData.info_a[i] // if (str) { // const a = builder.createString(str) // infoAs.push(a) // } // } // const pbInfoA = tables.FlattedData.createInfoAVector(builder, infoAs) // tables.FlattedData.startFlattedData(builder) // tables.FlattedData.addPositions(builder, pbPositions) // tables.FlattedData.addIndices(builder, pbIndices) // tables.FlattedData.addInfoA(builder, pbInfoA) // tables.FlattedData.addInfoB(builder, pbInfoB) // const fbData = tables.FlattedData.endFlattedData(builder) // builder.finish(fbData) // // 這個buffer是有偏移量的 // return builder.asUint8Array().slice().buffer // // return builder.asUint8Array().buffer }
// 首先引入基礎庫 const flatbuffers = require('flatbuffers').flatbuffers // 而後引入編譯出的JS模塊 const tables = require('src/generated/data_generated.js') const flatbufferParse = buffer => { buffer = new Uint8Array(buffer) buffer = new flatbuffers.ByteBuffer(buffer) const _data = tables.Data.getRootAsData(buffer) // 讀數據(flatbuffer在解析時並不讀取數據,所以這裏須要主動讀) let _read = null const len = _data.itemsLength() for (let i = 0; i < len; i++) { const item = _data.items(i) const info = item.info() _read = info.a() _read = info.b() _read = item.index() _read = item.positionArray() } // 扁平化方案: // buffer = new Uint8Array(buffer) // buffer = new flatbuffers.ByteBuffer(buffer) // const _data = tables.FlattedData.getRootAsFlattedData(buffer) // // 讀數據(flatbuffer是使用get函數延遲讀取的,所以這裏須要主動讀取數據) // let _read = null // _read = _data.positionsArray() // _read = _data.indicesArray() // _read = _data.infoBArray() // const len = _data.infoALength() // for (let i = 0; i < len; i++) { // _read = _data.infoA(i) // } }
Flatbuffers 對字符串的解析性能較差,當數據中的字符串佔比較高時,其總體序列化性能、解析性能和體積都不如JSON,對於純數值數據,相對於JSON優點明顯。其狀態機通常的接口設計對於複雜數據結構的構建比較繁瑣。
測試環境:15' MBP mid 2015,2.2 GHz Intel Core i7,16 GB 1600 MHz DDR3,macOS 10.14.3,Chrome 75
測試數據:上面例子中的數據,200,000條,字符串使用 UUID*2
測試方式:運行10次取平均值,GZip使用默認配置 gzip ./*
單位:時間 ms,體積 Mb
從測試結果來看,若是你的場景對性能有較高要求,將數據扁平化老是明智的原則。
本文爲雲棲社區原創內容,未經容許不得轉載。