JSON 中无精度损失的浮点数存储
46420 给这篇文章评分:
3.2
JSON 中无精度损失的浮点数存储
匿名 2015年11月19日,星期四
最近,我和一位同事就 JSON 作为数据存储和传输格式的优缺点进行了一场辩论。至少与 XML 相比,它的简洁性和高效性帮助它成为了客户端/服务器通信乃至数据/元数据存储的主导技术之一。但存在一个潜在的缺点,即它是一种文本格式,而不是二进制格式,因此当浮点数编码到 JSON 流中时,不可避免地会损失精度。数字到字符串或字符串到数字的转换可能导致截断或舍入误差,特别是当两种转换使用不同的编程语言时。
我们需要的是在 JSON 编码时,以二进制形式或等效形式表达浮点数。我们有几个选项,它们在编码效率和实现复杂性上各不相同。我将展示如何在 IDL 中对这些选项进行编码和解码,从最简单但效率最低的开始。请注意,我并未对字节序做任何处理,因此,如果您需要在大小端系统之间通信,则需要修改此代码,使其始终以给定的字节序编码值,然后为另一系统处理转换。
浮点数转字节数组
IDL 具有将浮点数(Float)或双精度浮点数(Double)转换为字节数组(Array of Bytes)的能力,类似于 C++ 中的 reinterpret_cast<>。帮助文档中的解释有些令人困惑,但您可以为类型转换函数 BYTE()、FIX()、LONG()……指定一个偏移量和一组维度。当您这样做时,它会访问存储浮点数的内存块,并将其视为一个字节数组。请注意,偏移量参数以及维度参数都是针对输出数据类型的索引,而不是输入数据类型:
IDL> byte(!pi) 3 IDL> byte(!pi, 0) 219 IDL> byte(!pi, 1) 15 IDL> byte(!pi, 2) 73 IDL> byte(!pi, 3) 64 IDL> byte(!pi, 0, 4) 219 15 73 64
我们可以看到,仅使用一个参数调用 Byte() 时,会执行类型强制转换,将 Pi 变为字节值 3。但是,一旦我们开始使用偏移量参数,我们就可以从用于存储浮点数的 4 个字节块中逐个获取字节。使用两个额外的参数可以让我们获得包含 4 个字节的数组。转换回浮点数的方法类似:
IDL> float([219b, 15b, 73b, 64b], 0, 1) 3.1415927
使用这些函数,我们可以构建递归函数来编码和解码表示已解析 JSON 文档的列表(List)和哈希表(Hash)。编码函数遍历 List 和 Hash 对象,对 List 和 Hash 元素进行递归,复制非浮点数元素,并转换浮点数(Float)和双精度浮点数(Double)的标量及数组。虽然我们可以直接用字节数组替换浮点数,但我们不希望将这些编码后的形式与普通的字节数组混淆。因此,我们创建一个新的 Hash 对象,在 "bytes" 键下存储字节数组,在 "encoding" 键下存储编码类型。编码键的值是我们用于将字节数组解码回浮点数或双精度浮点数的函数名。这种行为可以扩展到复数,而 JSON 本身根本不支持复数。解码函数与编码函数类似,遍历并对 List 和 Hash 元素进行递归。主要区别在于,当它找到一个包含 "encoding" 和 "bytes" 键的 Hash 时,它会调用相应的函数将字节数组转换回浮点数或双精度浮点数值。将浮点值编码为字节数组时,始终将所需的字节数作为第一个维度,因此标量浮点数变为一个 4 元素的字节数组,而一个 [M, N] 浮点数数组变为一个 [4, M, N] 的字节数组。如果传入标量浮点数或双精度浮点数,Size(/DIMENSION) 函数将返回 0,因此我们使用三元运算符将输入维度设置为标量 1。这意味着字节数组的维度是 [4, 1] 或 [8, 1],IDL 会自动将其转换为 1 维数组。解码时,我们进行简单的验证,确保第一个维度是正确的数字(4 或 8),然后将其剥离以获得浮点值的维度。
以下是完整的代码:
function ByteArrayToFloat, bytes compile_opt idl2 dims = Size(bytes, /DIMENSION) if (dims[0] ne 4) then Message, 'Invalid byte array, first dimension must be 4' floatDims = (Size(bytes, /N_DIMENSION) eq 1) ? 1 : dims[1:*] return, Float(bytes, 0, floatDims) end function ByteArrayToDouble, bytes compile_opt idl2 dims = Size(bytes, /DIMENSION) if (dims[0] ne 8) then Message, 'Invalid byte array, first dimension must be 8' doubleDims = (Size(bytes, /N_DIMENSION) eq 1) ? 1 : dims[1:*] return, Double(bytes, 0, doubleDims) end function decodeJSONFloatFromByteArray, inJson compile_opt idl2 if (ISA(inJson, 'Hash')) then begin if (inJson.HasKey('encoding') && inJson.HasKey('bytes')) then begin if (inJson['encoding'] eq 'ByteArrayToFloat') then begin return, ByteArrayToFloat(inJson['bytes']) endif else if (inJson['encoding'] eq 'ByteArrayToDouble') then begin return, ByteArrayToDouble(inJson['bytes']) endif endif else begin outJson = Hash() foreach val, inJson, key do begin outJson[key] = decodeJSONFloatFromByteArray(val) endforeach return, outJson endelse endif else if (ISA(inJson, 'List')) then begin outJson = List() foreach el, inJson, index do begin outJson.Add, decodeJSONFloatFromByteArray(el) endforeach return, outJson endif return, inJson end function encodeJSONFloatToByteArray, inJson compile_opt idl2 if (ISA(inJson, 'Hash')) then begin outJson = Hash() foreach val, inJson, key do begin outJson[key] = encodeJSONFloatToByteArray(val) endforeach return, outJson endif else if (ISA(inJson, 'List')) then begin outJson = List() foreach el, inJson, index do begin outJson.Add, encodeJSONFloatToByteArray(el) endforeach return, outJson endif else begin if (ISA(inJson, 'Float')) then begin dims = ISA(inJson, /SCALAR) ? 1 : Size(inJson, /DIMENSION) byteDims = [ 4, dims ] return, Hash('encoding', 'ByteArrayToFloat', 'bytes', Byte(inJson, 0, byteDims)) endif else if (ISA(inJson, 'Double')) then begin dims = ISA(inJson, /SCALAR) ? 1 : Size(inJson, /DIMENSION) byteDims = [ 8, dims ] return, Hash('encoding', 'ByteArrayToDouble', 'bytes', Byte(inJson, 0, byteDims)) endif endelse return, inJson end
编码和解码函数的使用很容易理解:
IDL> j = List(Hash('factory', 'foo', 'value', Findgen(3,2)), Hash('factory', 'bar', 'val', Dindgen(4,3))) IDL> j [ { "factory": "foo", "value": [[0.00000000, 1.0000000, 2.0000000], [3.0000000, 4.0000000, 5.0000000]] }, { "factory": "bar", "val": [[0.00000000000000000, 1.0000000000000000, 2.0000000000000000, 3.0000000000000000], [4.0000000000000000, 5.0000000000000000, 6.0000000000000000, 7.0000000000000000], [8.0000000000000000, 9.0000000000000000, 10.000000000000000, 11.000000000000000]] } ] IDL> j2 = encodeJSONFloattoByteArray(j) IDL> j2 [ { "factory": "foo", "value": { "bytes": [[[0, 0, 0, 0], [0, 0, 128, 63], [0, 0, 0, 64]], [[0, 0, 64, 64], [0, 0, 128, 64], [0, 0, 160, 64]]], "encoding": "ByteArrayToFloat" } }, { "factory": "bar", "val": { "bytes": [[[0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 240, 63], [0, 0, 0, 0, 0, 0, 0, 64], [0, 0, 0, 0, 0, 0, 8, 64]], [[0, 0, 0, 0, 0, 0, 16, 64], [0, 0, 0, 0, 0, 0, 20, 64], [0, 0, 0, 0, 0, 0, 24, 64], [0, 0, 0, 0, 0, 0, 28, 64]], [[0, 0, 0, 0, 0, 0, 32, 64], [0, 0, 0, 0, 0, 0, 34, 64], [0, 0, 0, 0, 0, 0, 36, 64], [0, 0, 0, 0, 0, 0, 38, 64]]], "encoding": "ByteArrayToDouble" } } ] IDL> j3 = decodeJSONFloatFromByteArray(j2) IDL> j3 [ { "factory": "foo", "value": [[0.00000000, 1.0000000, 2.0000000], [3.0000000, 4.0000000, 5.0000000]] }, { "factory": "bar", "val": [[0.00000000000000000, 1.0000000000000000, 2.0000000000000000, 3.0000000000000000], [4.0000000000000000, 5.0000000000000000, 6.0000000000000000, 7.0000000000000000], [8.0000000000000000, 9.0000000000000000, 10.000000000000000, 11.000000000000000]] } ]
正如我之前提到的,更容易实现的代码会导致编码效率降低。在这种情况下,调用 JSON_Serialize(j) 返回的字符串长度为 139 个字符,而 JSON_Serialize(j2) 返回的字符串为 455 个字符,扩大了 227%。另一个问题是,对于我们可能发送编码 JSON 流的其他语言来说,这并不容易实现。
浮点数转十六进制字符串
浮点数更常见的表示形式是原始二进制表示的十六进制字符串。我们不是将每个浮点数或双精度浮点数转换为 4 或 8 元素的字节数组,而是将其转换为由这些字节的十六进制值构建的 8 或 16 字符字符串。有几种方法可以做到这一点,但我采用了将浮点数数组转换为具有相同维度和适当位深度(浮点数用 ULong,双精度浮点数用 ULong64)的无符号整数数组的方法。我们可以从浮点数转换到字节数组再到 ULong,但在这里没有必要,因为 ULong() 函数将执行适当的 reinterpret_cast<> 操作。然后,我使用 String() 函数中的 FORMAT 关键字将 ULong 值转换为字符串,以指定十六进制输出。我本可以使用简单的 FORMAT='(Z)',但我喜欢确定性,所以我使用 '(Z08)' 和 '(Z016)' 来始终输出相同数量的字符,将可以用少于 8(或 16)个字符表示的字符串用零填充。转换为字符串确实会产生一个 1 维数组,因此我使用 Reform() 将其转换为预期的维度。与第一种情况相比,这种编码的一个优点是数据维度没有改变。
解码十六进制字符串基本上是逆过程。我获取十六进制字符串的维度,并分配一个具有相同维度的 ULong 或 ULong64 数组。然后使用 READS 从字符串读取到长整型,并使用 FORMAT 关键字设置为告诉它字符串是十六进制格式。我可以将格式设置为 '(Z)',它会将所有字符串解析为相应的长整型。然后,我将长整型传递到 Float 或 Double 函数中,偏移量为 0,维度为十六进制字符串的维度。
以下是完整的代码:
function HexStringToFloat, hexStr compile_opt idl2 dims = ISA(hexStr, /SCALAR) ? 1 : Size(hexStr, /DIMENSION) longs = ULonArr(dims, /NOZERO) Reads, hexStr, longs, FORMAT='(Z)' return, Float(longs, 0, dims) end function HexStringToDouble, hexStr compile_opt idl2 dims = ISA(hexStr, /SCALAR) ? 1 : Size(hexStr, /DIMENSION) longs = ULon64Arr(dims, /NOZERO) Reads, hexStr, longs, FORMAT='(Z)' return, Double(longs, 0, dims) end function decodeJSONFloatFromHexString, inJson compile_opt idl2 if (ISA(inJson, 'Hash')) then begin if (inJson.HasKey('encoding') && inJson.HasKey('hex_string')) then begin if (inJson['encoding'] eq 'HexStringToFloat') then begin return, HexStringToFloat(inJson['hex_string']) endif else if (inJson['encoding'] eq 'HexStringToDouble') then begin return, HexStringToDouble(inJson['hex_string']) endif endif else begin outJson = Hash() foreach val, inJson, key do begin outJson[key] = decodeJSONFloatFromHexString(val) endforeach return, outJson endelse endif else if (ISA(inJson, 'List')) then begin outJson = List() foreach el, inJson, index do begin outJson.Add, decodeJSONFloatFromHexString(el) endforeach return, outJson endif return, inJson end function encodeJSONFloatToHexString, inJson compile_opt idl2 if (ISA(inJson, 'Hash')) then begin outJson = Hash() foreach val, inJson, key do begin outJson[key] = encodeJSONFloatToHexString(val) endforeach return, outJson endif else if (ISA(inJson, 'List')) then begin outJson = List() foreach el, inJson, index do begin outJson.Add, encodeJSONFloatToHexString(el) endforeach return, outJson endif else begin if (ISA(inJson, 'Float')) then begin dims = ISA(inJson, /SCALAR) ? 1 : Size(inJson, /DIMENSION) longs = ULong(inJson, 0, dims) hex = String(longs, FORMAT='(Z08)') return, Hash('encoding', 'HexStringToFloat', 'hex_string', Reform(hex, dims)) endif else if (ISA(inJson, 'Double')) then begin dims = ISA(inJson, /SCALAR) ? 1 : Size(inJson, /DIMENSION) longs = ULong64(inJson, 0, dims) hex = String(longs, FORMAT='(Z016)') return, Hash('encoding', 'HexStringToDouble', 'hex_string', Reform(hex, dims)) endif endelse return, inJson end
这些编码和解码函数的效果如下所示:
IDL> j = List(hash('factory', 'foo', 'value', findgen(3,2)), hash('factory', 'bar', 'val', dindgen(4,3))) IDL> j [ { "factory": "foo", "value": [[0.00000000, 1.0000000, 2.0000000], [3.0000000, 4.0000000, 5.0000000]] }, { "factory": "bar", "val": [[0.00000000000000000, 1.0000000000000000, 2.0000000000000000, 3.0000000000000000], [4.0000000000000000, 5.0000000000000000, 6.0000000000000000, 7.0000000000000000], [8.0000000000000000, 9.0000000000000000, 10.000000000000000, 11.000000000000000]] } ] IDL> j2 = encodeJSONFloatToHexString(j) IDL> j2 [ { "factory": "foo", "value": { "hex_string": [["00000000", "3F800000", "40000000"], ["40400000", "40800000", "40A00000"]], "encoding": "HexStringToFloat" } }, { "factory": "bar", "val": { "hex_string": [["0000000000000000", "3FF0000000000000", "4000000000000000", "4008000000000000"], ["4010000000000000", "4014000000000000", "4018000000000000", "401C000000000000"], ["4020000000000000", "4022000000000000", "4024000000000000", "4026000000000000"]], "encoding": "HexStringToDouble" } } ] IDL> j3 = decodeJSONFloatFromHexString(j2) IDL> j3 [ { "factory": "foo", "value": [[0.00000000, 1.0000000, 2.0000000], [3.0000000, 4.0000000, 5.0000000]] }, { "factory": "bar", "val": [[0.00000000000000000, 1.0000000000000000, 2.0000000000000000, 3.0000000000000000], [4.0000000000000000, 5.0000000000000000, 6.0000000000000000, 7.0000000000000000], [8.0000000000000000, 9.0000000000000000, 10.000000000000000, 11.000000000000000]] } ]
这种编码方案的效率略高于第一种,j2 为 450 个字符,而第一种为 455。这可能看起来不多,但 450 是非常确定的,因为每个浮点数产生 8 个字符,每个双精度浮点数产生 16 个字符。第一个编码器的 455 个字符几乎是最佳情况,因为有很多 0 字节,这些字节只需要 1 个字符。平均而言,一个字节应该需要 2.57 个字符(10 个 1 字符值,90 个 2 字符值,156 个 3 字符值)。原始 JSON 流中的 139 个字符也相当精简,因为每个浮点值都是精确的整数,所以它只使用了小数点右边的一位数字。
让我们考虑更真实的情况,例如一个 2D 随机数数组:
IDL> orig = randomu(1, 4, 5) IDL> orig 0.41702199 0.99718481 0.72032452 0.93255734 0.00011438108 0.12812445 0.30233258 0.99904054 0.14675589 0.23608898 0.092338592 0.39658073 0.18626021 0.38791075 0.34556073 0.66974604 0.39676747 0.93553907 0.53881675 0.84631091 IDL> byte_array = encodeJSONFloatToByteArray(orig) IDL> byte_array { "bytes": [[[232, 131, 213, 62], [129, 71, 127, 63], [48, 103, 56, 63], [20, 188, 110, 63]], [[224, 223, 239, 56], [14, 51, 3, 62], [86, 203, 154, 62], [31, 193, 127, 63]], [[45, 71, 22, 62], [79, 193, 113, 62], [4, 28, 189, 61], [161, 12, 203, 62]], [[255, 186, 62, 62], [61, 156, 198, 62], [86, 237, 176, 62], [122, 116, 43, 63]], [[27, 37, 203, 62], [125, 127, 111, 63], [229, 239, 9, 63], [213, 167, 88, 63]]], "encoding": "ByteArrayToFloat" } IDL> hex_string = encodeJSONFloatToHexString(orig) IDL> hex_string { "hex_string": [["3ED583E8", "3F7F4781", "3F386730", "3F6EBC14"], ["38EFDFE0", "3E03330E", "3E9ACB56", "3F7FC11F"], ["3E16472D", "3E71C14F", "3DBD1C04", "3ECB0CA1"], ["3E3EBAFF", "3EC69C3D", "3EB0ED56", "3F2B747A"], ["3ECB251B", "3F6F7F7D", "3F09EFE5", "3F58A7D5"]], "encoding": "HexStringToFloat" } IDL> StrLen(JSON_Serialize(orig)), StrLen(JSON_Serialize(byte_array)), StrLen(JSON_Serialize(hex_string)) 394 364 276
对于双精度浮点数(Double)效果更好:
IDL> orig = randomu(1, 4, 5, /DOUBLE) IDL> orig 0.41702200470257400 0.72032449344215810 0.00011437481734488664 0.30233257263183977 0.14675589081711304 0.092338594768797799 0.18626021137767090 0.34556072704304774 0.39676747423066994 0.53881673400335695 0.41919451440329480 0.68521950039675950 0.20445224973151743 0.87811743639094542 0.027387593197926163 0.67046751017840223 0.41730480236712697 0.55868982844575166 0.14038693859523377 0.19810148908487879 IDL> byte_array = encodeJSONFloatToByteArray(orig) IDL> byte_array { "bytes": [[[6, 60, 250, 15, 125, 176, 218, 63], [81, 240, 186, 243, 229, 12, 231, 63], [0, 192, 97, 102, 144, 251, 29, 63], [244, 8, 254, 183, 106, 89, 211, 63]], [[60, 5, 199, 163, 229, 200, 194, 63], [16, 202, 176, 140, 128, 163, 183, 63], [228, 225, 52, 230, 95, 215, 199, 63], [206, 163, 91, 189, 170, 29, 214, 63]], [[232, 251, 123, 103, 163, 100, 217, 63], [84, 159, 98, 151, 252, 61, 225, 63], [138, 149, 129, 58, 21, 212, 218, 63], [39, 35, 25, 114, 81, 237, 229, 63]], [[12, 98, 24, 199, 125, 43, 202, 63], [74, 22, 235, 188, 137, 25, 236, 63], [192, 172, 103, 68, 126, 11, 156, 63], [169, 229, 167, 71, 120, 116, 229, 63]], [[254, 90, 168, 51, 31, 181, 218, 63], [11, 9, 185, 125, 201, 224, 225, 63], [220, 170, 6, 255, 50, 248, 193, 63], [68, 72, 116, 188, 99, 91, 201, 63]]], "encoding": "ByteArrayToDouble" } IDL> hex_string = encodeJSONFloatToHexString(orig) IDL> hex_string { "hex_string": [["3FDAB07D0FFA3C06", "3FE70CE5F3BAF051", "3F1DFB906661C000", "3FD3596AB7FE08F4"], ["3FC2C8E5A3C7053C", "3FB7A3808CB0CA10", "3FC7D75FE634E1E4", "3FD61DAABD5BA3CE"], ["3FD964A3677BFBE8", "3FE13DFC97629F54", "3FDAD4153A81958A", "3FE5ED5172192327"], ["3FCA2B7DC718620C", "3FEC1989BCEB164A", "3F9C0B7E4467ACC0", "3FE5747847A7E5A9"], ["3FDAB51F33A85AFE", "3FE1E0C97DB9090B", "3FC1F832FF06AADC", "3FC95B63BC744844"]], "encoding": "HexStringToDouble" } IDL> StrLen(JSON_Serialize(orig)), StrLen(JSON_Serialize(byte_array)), StrLen(JSON_Serialize(hex_string)) 391 659 437
浮点数数组的十六进制字符串编码占用了字节数组编码空间的 75.8%,而双精度浮点数数组的编码仅占用了字节数组编码空间的 66.3%。