当转换为Protobuf / C ++ float时,Python float何时会失去精度?

时间:2018-01-29 16:20:33

标签: python protocol-buffers

我有兴趣尽量减少从Python序列化的protobuf消息的大小。

Protobuf有浮点数(4个字节)和双精度数(8个字节)。 Python有一个浮点类型,实际上是C double,至少在CPython中。

我的问题是:给定一个Python float的实例,是否有"快速"如果将值分配给protobuf float(或实际上是C ++浮点数),则检查该值是否会失去精度?

3 个答案:

答案 0 :(得分:4)

您可以检查将float转换为十六进制表示形式;符号,指数和分数各自得到一个单独的部分。如果分数仅使用前6个十六进制数字(其余7个数字必须为零),则第6个数字为偶数(因此最后一个位设置)将你的64位双浮点适合32位单。指数限制在介于-126和127之间的值:

import math
import re

def is_single_precision(
        f,
        _isfinite=math.isfinite,
        _singlepat=re.compile(
            r'-?0x[01]\.[0-9a-f]{5}[02468ace]0{7}p'
            r'(?:\+(?:1[01]\d|12[0-7]|[1-9]\d|\d)|'
            r'-(?:1[01]\d|12[0-6]|[1-9]\d|\d))$').match):
    return not _isfinite(f) or _singlepat(f.hex()) is not None or f == 0.0

float.hex()方法非常快,比通过struct或numpy往返更快;你可以在不到半秒内创建100万个十六进制表示:

>>> timeit.Timer('(1.2345678901e+26).hex()').autorange()
(1000000, 0.47934128501219675)

正则表达式引擎也非常快,并且在上面的函数中优化了名称查找,我们可以在大约1.1秒内测试100万个浮点值:

>>> import random, sys
>>> testvalues = [0.0, float('inf'), float('-inf'), float('nan')] + [random.uniform(sys.float_info.min, sys.float_info.max) for _ in range(2 * 10 ** 6)]
>>> timeit.Timer('is_single_precision(f())', 'from __main__ import is_single_precision, testvalues; f = iter(testvalues).__next__').autorange()
(1000000, 1.1044921400025487)

上述方法有效,因为浮点数的 binary32 格式为分数分配了23位。指数被分配8位(有符号)。正则表达式只允许设置前23位,并且指数在有符号的8位数字的范围内。

另见

但这可能不是你想要的!以1/3或1/10为例。两者都是在浮点值中需要近似的值,并且都未通过测试:

>>> (1/3).hex()
'0x1.5555555555555p-2'
>>> (1/10).hex()
'0x1.999999999999ap-4'

您可能不得不采取启发式方法;如果您的十六进制值在分数的前6位数中全部为零,或者在(-126,127)范围之外的指数,则转换为double将导致太多损失。

答案 1 :(得分:2)

为了完整起见,这里是评论中提到的“round tripping through struct”方法,它的好处是不需要numpy但仍能给出准确的结果:

import struct, math
def is_single_precision_struct(x, _s=struct.Struct("f")):
    return math.isnan(x) or _s.unpack(_s.pack(x))[0] == x

is_single_precision_numpy()的时间比较:

  • is_single_precision_numpy(f):[2.5650789737701416,2.5488431453704834,2.551704168319702]
  • is_single_precision_struct(f):[0.3972139358520508,0.39684605598449707,0.39119601249694824]

所以它在我的机器上似乎也更快。

答案 2 :(得分:1)

如果您想要一个涵盖几乎所有极端情况的简单解决方案,并且能够正确检测超出范围的指数以及从较小精度中丢失信息,您可以使用NumPy进行转换您的潜在浮动到np.float32对象,然后与原始对象进行比较:

import numpy

def is_single_precision_numpy(floatval, _float32=np.float32):
    return _float32(floatval) == floatval

这会自动处理可能存在问题的情况,例如float32次正常范围内的值。例如:

>>> is_single_precision_numpy(float.fromhex('0x13p-149'))
True
>>> is_single_precision_numpy(float.fromhex('0x13.8p-149'))
False

使用基于hex的解决方案很难处理这些案例。

虽然不如@Martijn Pieters'基于正则表达式的解决方案,速度仍然可观(大约是基于正则表达式的解决方案的一半)。以下是时间安排(其中is_single_precision_re_hex正是Martijn的回答中的版本)。

>>> timeit.Timer('is_single_precision_numpy(f)', 'f = 1.2345678901e+26; from __main__ import is_single_precision_numpy').repeat(3, 10**6)
[2.035495020012604, 2.0115931580075994, 2.013475093001034]
>>> timeit.Timer('is_single_precision_re_hex(f)', 'f = 1.2345678901e+26; from __main__ import is_single_precision_re_hex').repeat(3, 10**6)
[1.1169273109990172, 1.1178153319924604, 1.1184561859990936]

不幸的是,虽然几乎所有极端情况(次正规,无穷大,有符号零,溢出等)都得到了正确处理,但是这个解决方案不适用的一个例子就是:{ {1}}是NaN。在这种情况下,floatval将返回is_single_precision_numpy。这可能对您的需求有影响,也可能无关紧要。如果它确实重要,那么添加额外的False检查应该可以解决问题:

isnan