将浮点数与整数进行比较时,某些值对的评估时间比其他类似值的值要长得多 .
例如:
>>> import timeit
>>> timeit.timeit("562949953420000.7 < 562949953421000") # run 1 million times
0.5387085462592742
但是如果浮点数或整数变小或变大一定量,则比较运行得更快:
>>> timeit.timeit("562949953420000.7 < 562949953422000") # integer increased by 1000
0.1481498428446173
>>> timeit.timeit("562949953423001.8 < 562949953421000") # float increased by 3001.1
0.1459577925548956
更改比较运算符(例如,使用 ==
或 >
)不会以任何明显的方式影响时间 .
这并不仅仅与幅度有关,因为选择更大或更小的值可以导致更快的比较,所以我怀疑它是由于位排列的一些不幸的方式 .
显然,对于大多数用例来说,比较这些值的速度要快得多 . 我只是好奇为什么Python似乎更多地使用一些值而不是其他值 .
2 回答
使用具有任意精度浮点数和整数的
gmpy2
可以获得更均匀的比较性能:float对象的Python源代码中的注释确认:
在将float与整数进行比较时尤其如此,因为与浮点数不同,Python中的整数可以任意大并且总是精确的 . 尝试将整数转换为浮点数可能会失去精度并使比较不准确 . 试图将浮点数转换为整数也不会起作用,因为任何小数部分都将丢失 .
为了解决这个问题,Python执行一系列检查,如果其中一个检查成功则返回结果 . 它比较两个值的符号,然后整数是否“太大”而不是浮点数,然后将浮点数的指数与整数的长度进行比较 . 如果所有这些检查都失败,则需要构造两个新的Python对象进行比较以获得结果 .
将float
v
与整数/长w
进行比较时,最糟糕的情况是:v
和w
具有相同的符号(均为正数或均为负数),整数
w
具有足够的位,可以保存在size_t类型中(通常为32或64位),整数
w
至少有49位,float
v
的指数与w
中的位数相同 .这正是我们对问题中的 Value 观的看法:
我们看到49既是float的指数又是整数中的位数 . 这两个数字都是正数,因此符合上述四个标准 .
选择其中一个值更大(或更小)可以更改整数的位数或指数的值,因此Python可以在不执行昂贵的最终检查的情况下确定比较结果 .
这特定于CPython语言的实现 .
更详细的比较
float_richcompare函数处理两个值
v
和w
之间的比较 .以下是该功能执行的检查的逐步说明 . 在尝试理解函数的功能时,Python源代码中的注释实际上非常有用,因此我将它们放在相关的位置 . 我还在答案的最后列表中总结了这些检查 .
主要思想是将Python对象
v
和w
映射到两个适当的C双精度i
和j
,然后可以轻松比较它们以得到正确的结果 . Python 2和Python 3都使用相同的想法来执行此操作(前者只分别处理int
和long
类型) .要做的第一件事是检查
v
绝对是一个Python浮点数并将其映射到C doublei
. 接下来,该函数查看w
是否也是一个float并将其映射到C doublej
. 这是函数的最佳情况,因为可以跳过所有其他检查 . 该函数还会检查v
是inf
还是nan
:现在我们知道如果
w
失败了这些检查,它就不是Python浮点数 . 现在该函数检查它是否是Python整数 . 如果是这种情况,最简单的测试是提取v
的符号和w
的符号(如果为零则返回0
,如果为负则返回-1
,如果为正则返回1
) . 如果符号不同,这是返回比较结果所需的所有信息:如果此检查失败,则
v
和w
具有相同的符号 .下一次检查计算整数
w
中的位数 . 如果它有太多位,那么它不可能被保持为浮点数,因此必须比floatv
更大:另一方面,如果整数
w
有48位或更少位,它可以安全地输入C doublej
并进行比较:从这一点开始,我们知道
w
有49位或更多位 . 将w
视为正整数会很方便,所以要改变必要时标志和比较运算符:现在该函数查看float的指数 . 回想一下,浮点数可以写成(忽略符号)作为有效数字* 2exponent,有效数字表示0.5和1之间的数字:
这检查了两件事 . 如果指数小于0,则浮点数小于1(并且其幅度小于任何整数) . 或者,如果指数小于
w
中的位数,那么我们有v < |w|
,因为有效数* 2exponent小于2nbits .如果这两个检查失败,该函数会查看指数是否大于
w
中的位数 . 这表明有效数* 2exponent大于2nbits,所以v > |w|
:如果此检查未成功,我们知道float
v
的指数与整数w
中的位数相同 .现在可以比较这两个值的唯一方法是从
v
和w
构造两个新的Python整数 . 想法是丢弃v
的小数部分,将整数部分加倍,然后加1 .w
也加倍,可以比较这两个新的Python对象以提供正确的返回值 . 使用具有较小值的示例,4.65 < 4
将由比较(2*4)+1 == 9 < 8 == (2*4)
(返回false)确定 .为简洁起见,我遗漏了额外的错误检查和垃圾跟踪Python在创建这些新对象时必须做的事情 . 毋庸置疑,这会增加额外的开销,并解释为什么问题中突出显示的值比其他值要慢得多 .
以下是比较功能执行的检查的摘要 .
设
v
为浮点数并将其转换为C double . 现在,如果w
也是一个浮点数:检查
w
是nan
还是inf
. 如果是这样,请根据w
的类型单独处理此特殊情况 .如果不是,请将
v
和w
直接与其表示形式进行比较为C双打 .如果
w
是一个整数:提取
v
和w
的标志 . 如果它们不同,那么我们知道v
和w
是不同的,哪个是更大的值 .(符号相同 . )检查
w
是否有太多位而不是浮点数(大于size_t
) . 如果是这样,w
的幅度大于v
.检查
w
是否有48位或更少位 . 如果是这样,它可以安全地转换为C double而不会失去其精度并与v
进行比较 .(
w
有超过48位 . 我们现在将w
视为正整数,并在适当时更改了比较操作 . )考虑浮点
v
的指数 . 如果指数为负,则v
小于1
,因此小于任何正整数 . 否则,如果指数小于w
中的位数,则它必须小于w
.如果
v
的指数大于w
中的位数,则v
大于w
.(指数与
w
中的位数相同 . )最后检查 . 将
v
拆分为整数和小数部分 . 将整数部分加倍并加1以补偿小数部分 . 现在加倍整数w
. 比较这两个新的整数来获得结果 .