为什么有些浮点<整数比较比其他四倍慢?
当将浮点数与整数相比较时,某些值对需要比其他类似的值更长的时间才能被评估。
例如:
>>> 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
更改比较运算符(例如,使用==
或>
)不会以明显的方式影响时间。
这并不仅仅与数量有关,因为select更大或更小的值可能会导致更快的比较,所以我怀疑是不幸的,这些比特排列起来。
显然,比较这些值对于大多数用例而言足够快。 我只是好奇,为什么Python似乎与其他一些值的斗争更多的价值。
对于浮动对象的Python源代码中的评论承认:
比较几乎是一场噩梦
将float与一个整数进行比较时尤其如此,因为与float不同的是,Python中的整数可以是任意大的且总是精确的。 试图将整数转换为浮点数可能会失去精度并使比较不准确。 试图将浮点数转换为整数不会工作,因为任何小数部分都将丢失。
为了解决这个问题,Python执行一系列检查,如果其中一个检查成功,则返回结果。 它比较两个值的符号,然后是整数是“太大”作为一个浮点数,然后比较浮点数的指数与整数的长度。 如果所有这些检查都失败了,则需要构造两个新的Python对象进行比较以获得结果。
当比较浮点数v
与整数/长整数w
,最坏的情况是:
-
v
和w
有相同的符号(包括正数或负数), - 整数
w
位数足够less,可以保存在size_t
types(通常为32或64位)中, - 整数
w
至less有49位, - 浮点数
v
的指数与w
的位数相同。
这正是我们对于这个问题的价值所具有的:
>>> import math >>> math.frexp(562949953420000.7) # gives the float's (significand, exponent) pair (0.9999999999976706, 49) >>> (562949953421000).bit_length() 49
我们可以看到,49既是浮点数的指数又是整数中的位数。 这两个数字都是正数,因此符合上述四个标准。
select一个更大(或更小)的值可以改变整数的位数或指数的值,所以Python可以在不执行昂贵的最终检查的情况下确定比较结果。
这是CPython语言的具体实现。
比较更详细
float_richcompare
函数处理两个值v
和w
之间的比较。
以下是对function执行的检查的逐步说明。 Python源代码中的注释对于理解函数的作用是非常有帮助的,所以我把它们留在了相关的地方。 我也在答案的底部列出了这些检查。
主要想法是将Python对象v
和w
映射到两个适当的C双精度, i
和j
,然后可以轻松地进行比较,以得到正确的结果。 Python 2和Python 3都使用相同的想法来做到这一点(前者分别处理int
和long
types)。
首先要做的是检查v
是一个Python浮点数,并将其映射到一个C double i
。 接下来,函数检查w
是否也是一个浮点数,并将其映射到一个C double j
。 这是function最好的情况下,所有其他检查可以跳过。 该函数还检查v
是inf
还是nan
:
static PyObject* float_richcompare(PyObject *v, PyObject *w, int op) { double i, j; int r = 0; assert(PyFloat_Check(v)); i = PyFloat_AS_DOUBLE(v); if (PyFloat_Check(w)) j = PyFloat_AS_DOUBLE(w); else if (!Py_IS_FINITE(i)) { if (PyLong_Check(w)) j = 0.0; else goto Unimplemented; }
现在我们知道如果w
没有通过这些检查,它不是一个Python浮点数。 现在函数检查它是否是一个Python整数。 如果是这样的话,最简单的testing就是提取v
的符号和w
的符号(如果为0
则返回0
否则返回-1
,如果为正则返回1
)。 如果符号不同,这是返回比较结果所需的全部信息:
else if (PyLong_Check(w)) { int vsign = i == 0.0 ? 0 : i < 0.0 ? -1 : 1; int wsign = _PyLong_Sign(w); size_t nbits; int exponent; if (vsign != wsign) { /* Magnitudes are irrelevant -- the signs alone * determine the outcome. */ i = (double)vsign; j = (double)wsign; goto Compare; } }
如果检查失败,则v
和w
具有相同的符号。
下一次检查计算整数w
的位数。 如果它的位数太多,那么就不可能把它当作浮点数,因此必须大于浮点数v
:
nbits = _PyLong_NumBits(w); if (nbits == (size_t)-1 && PyErr_Occurred()) { /* This long is so large that size_t isn't big enough * to hold the # of bits. Replace with little doubles * that give the same outcome -- w is so large that * its magnitude must exceed the magnitude of any * finite float. */ PyErr_Clear(); i = (double)vsign; assert(wsign != 0); j = wsign * 2.0; goto Compare; }
另一方面,如果整数w
有48或更less的位,它可以安全地转入一个C双j
并进行比较:
if (nbits <= 48) { j = PyLong_AsDouble(w); /* It's impossible that <= 48 bits overflowed. */ assert(j != -1.0 || ! PyErr_Occurred()); goto Compare; }
从这一点开始,我们知道w
有49位或更多位。 将w
作为正整数会很方便,因此必须根据需要更改符号和比较运算符:
if (nbits <= 48) { /* "Multiply both sides" by -1; this also swaps the * comparator. */ i = -i; op = _Py_SwappedOp[op]; }
现在函数查看浮点数的指数。 回想一下,浮点数可以写成(忽略符号)为有效数* 2 指数 ,而有效数表示0.5到1之间的数字:
(void) frexp(i, &exponent); if (exponent < 0 || (size_t)exponent < nbits) { i = 1.0; j = 2.0; goto Compare; }
这检查两件事。 如果指数小于0,则浮点数小于1(幅度小于任何整数)。 或者,如果指数小于w
的位数,那么我们有v < |w|
因为有效数* 2 指数小于2 nbits 。
如果这两项检查失败,函数将查看指数是否大于w
的位数。 这表明有效数* 2 指数大于2 nbits ,所以v > |w|
:
if ((size_t)exponent > nbits) { i = 2.0; j = 1.0; goto Compare; }
如果这个检查没有成功,我们知道float v
的指数与整数w
的位数相同。
现在可以比较两个值的唯一方法是从v
和w
构造两个新的Python整数。 这个想法是放弃v
的小数部分,加倍整数部分,然后添加一个。 w
也加倍,这两个新的Python对象可以进行比较,以给出正确的返回值。 使用小值的示例, 4.65 < 4
将由比较(2*4)+1 == 9 < 8 == (2*4)
(返回false)来确定。
{ double fracpart; double intpart; PyObject *result = NULL; PyObject *one = NULL; PyObject *vv = NULL; PyObject *ww = w; // snip fracpart = modf(i, &intpart); // split i (the double that v mapped to) vv = PyLong_FromDouble(intpart); // snip if (fracpart != 0.0) { /* Shift left, and or a 1 bit into vv * to represent the lost fraction. */ PyObject *temp; one = PyLong_FromLong(1); temp = PyNumber_Lshift(ww, one); // left-shift doubles an integer ww = temp; temp = PyNumber_Lshift(vv, one); vv = temp; temp = PyNumber_Or(vv, one); // a doubled integer is even, so this adds 1 vv = temp; } // snip } }
为了简洁,我省略了额外的错误检查和垃圾跟踪Python创build这些新对象时必须做的事情。 不用说,这增加了额外的开销,并解释了为什么在问题中突出显示的值比其他值显着慢。
以下是由比较function执行的检查的总结。
让v
是一个浮点数,并把它作为一个C double。 现在,如果w
也是一个浮点数:
-
检查
w
是nan
还是inf
。 如果是这样,根据w
的types分别处理这个特殊情况。 -
如果不是,直接比较
v
和w
的表示为C双打。
如果w
是一个整数:
-
提取
v
和w
的符号。 如果它们不同,那么我们知道v
和w
是不同的,哪个是更大的价值。 -
( 符号相同。 )检查
w
是否有太多的位是浮点数(大于size_t
)。 如果是这样,w
幅度比v
更大。 -
检查
w
是否有48位或更less位。 如果是这样的话,可以安全地转换成C双精度,而不会失去精度。 -
(
w
有48位以上,我们现在将w
视为一个正整数,以适当的方式改变比较操作 )。 -
考虑float
v
的指数。 如果指数为负,则v
小于1
,因此小于任何正整数。 否则,如果指数小于w
的位数,那么它必须小于w
。 -
如果
v
的指数大于w
的位数,则v
大于w
。 -
( 指数与
w
的位数相同 ) -
最后的检查。 将
v
拆分成整数和小数部分。 将整数部分加倍并加1以补偿小数部分。 现在加倍整数w
。 比较这两个新的整数来获得结果。
使用任意精度的浮点数和整数的gmpy2
,可以获得更一致的比较性能:
~ $ ptipython Python 3.5.1 |Anaconda 4.0.0 (64-bit)| (default, Dec 7 2015, 11:16:01) Type "copyright", "credits" or "license" for more information. IPython 4.1.2 -- An enhanced Interactive Python. ? -> Introduction and overview of IPython's features. %quickref -> Quick reference. help -> Python's own help system. object? -> Details about 'object', use 'object??' for extra details. In [1]: import gmpy2 In [2]: from gmpy2 import mpfr In [3]: from gmpy2 import mpz In [4]: gmpy2.get_context().precision=200 In [5]: i1=562949953421000 In [6]: i2=562949953422000 In [7]: f=562949953420000.7 In [8]: i11=mpz('562949953421000') In [9]: i12=mpz('562949953422000') In [10]: f1=mpfr('562949953420000.7') In [11]: f<i1 Out[11]: True In [12]: f<i2 Out[12]: True In [13]: f1<i11 Out[13]: True In [14]: f1<i12 Out[14]: True In [15]: %timeit f<i1 The slowest run took 10.15 times longer than the fastest. This could mean that an intermediate result is being cached. 1000000 loops, best of 3: 441 ns per loop In [16]: %timeit f<i2 The slowest run took 12.55 times longer than the fastest. This could mean that an intermediate result is being cached. 10000000 loops, best of 3: 152 ns per loop In [17]: %timeit f1<i11 The slowest run took 32.04 times longer than the fastest. This could mean that an intermediate result is being cached. 1000000 loops, best of 3: 269 ns per loop In [18]: %timeit f1<i12 The slowest run took 36.81 times longer than the fastest. This could mean that an intermediate result is being cached. 1000000 loops, best of 3: 231 ns per loop In [19]: %timeit f<i11 The slowest run took 78.26 times longer than the fastest. This could mean that an intermediate result is being cached. 10000000 loops, best of 3: 156 ns per loop In [20]: %timeit f<i12 The slowest run took 21.24 times longer than the fastest. This could mean that an intermediate result is being cached. 10000000 loops, best of 3: 194 ns per loop In [21]: %timeit f1<i1 The slowest run took 37.61 times longer than the fastest. This could mean that an intermediate result is being cached. 1000000 loops, best of 3: 275 ns per loop In [22]: %timeit f1<i2 The slowest run took 39.03 times longer than the fastest. This could mean that an intermediate result is being cached. 1000000 loops, best of 3: 259 ns per loop