我有兴趣尽量减少从Python序列化的protobuf消息的大小。
Protobuf有浮点数(4个字节)和双精度数(8个字节)。 Python有一个浮点类型,实际上是C double,至少在CPython中。
我的问题是:给定一个Python float
的实例,是否有"快速"如果将值分配给protobuf float
(或实际上是C ++浮点数),则检查该值是否会失去精度?
答案 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()
的时间比较:
所以它在我的机器上似乎也更快。
答案 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