当 Python 浮点数转换为 Protobuf/C++ 浮点数时,什么时候会失去精度?



我有兴趣最小化从Python序列化的protobuf消息的大小。

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

我的问题是:给定一个 Pythonfloat的实例,是否有一种"快速"方法来检查如果将值分配给 protobuffloat(或实际上是C++浮点数)是否会失去精度?

您可以检查将浮点数转换为十六进制表示;符号、指数和分数各有一个单独的部分。假设分数仅使用前 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()方法非常快,比通过结构或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)

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

另请参阅

  • IEEE 754 单精度二进制浮点格式:二进制32
  • IEEE 754 双精度二进制浮点格式:二进制64

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

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

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

为了完整起见,这里是注释中提到的"结构往返">方法,它的好处是不需要 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]

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

如果您想要一个简单的解决方案,几乎涵盖所有极端情况,并且可以正确检测超出范围的指数以及较小精度的信息丢失,则可以使用 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]

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

import math
import numpy as np
def is_single_precision_numpy(floatval, _float32=np.float32, _isnan=math.isnan):
return _float32(floatval) == floatval or _isnan(floatval)

最新更新