Why are some float < integer comparisons four times slower than others?
当比较浮点数和整数时,某些成对的值的计算时间要比其他具有类似量级的值长得多。
例如:
1 2 3 | >>> import timeit >>> timeit.timeit("562949953420000.7 < 562949953421000") # run 1 million times 0.5387085462592742 |
但是,如果浮点或整数被缩小或增大一定数量,则比较运行得更快:
1 2 3 4 | >>> timeit.timeit("562949953420000.7 < 562949953422000") # integer increased by 1000 0.1481498428446173 >>> timeit.timeit("562949953423001.8 < 562949953421000") # float increased by 3001.1 0.1459577925548956 |
更改比较运算符(例如使用
这不仅仅与数量级有关,因为选择较大或较小的值会导致更快的比较,所以我怀疑这与位的排列方式有点不妥。
显然,对于大多数用例来说,比较这些值已经足够快了。我只是好奇为什么python在某些值对上比在其他值对上更费劲。
python源代码中对float对象的注释承认:好的。
Comparison is pretty much a nightmare
Ok.
在将浮点与整数进行比较时尤其如此,因为与浮点不同,python中的整数可以任意大且总是精确的。尝试将整数强制转换为浮点可能会丢失精度并导致比较不准确。尝试将浮点转换为整数也不会起作用,因为任何小数部分都将丢失。好的。
为了解决这个问题,Python执行一系列检查,如果其中一个检查成功,则返回结果。它比较两个值的符号,然后比较整数是否"太大"而不能成为浮点,然后比较浮点的指数与整数的长度。如果所有这些检查都失败了,那么有必要构建两个新的python对象进行比较,以获得结果。好的。
比较浮点数
v 和w 的符号相同(均为正或均为负)。- 整数
w 的位数足够少,可以保存在size_t 类型中(通常为32或64位)。 - 整数
w 至少有49位, - 浮点数
v 的指数与w 中的位数相同。
这正是我们对这个问题的价值观的看法:好的。
1 2 3 4 5 | >>> import math >>> math.frexp(562949953420000.7) # gives the float's (significand, exponent) pair (0.9999999999976706, 49) >>> (562949953421000).bit_length() 49 |
我们看到49同时是浮点的指数和整数的位数。这两个数字都是正数,因此满足上述四个标准。好的。
选择一个更大(或更小)的值可以更改整数的位数或指数的值,因此python能够在不执行昂贵的最终检查的情况下确定比较结果。好的。
这是特定于该语言的cpython实现的。好的。更详细的比较
下面是该函数执行的检查的逐步说明。当试图理解函数的作用时,python源代码中的注释实际上非常有用,所以我将它们放在相关的地方。我还将这些检查总结在答案底部的列表中。好的。
主要的思想是将python对象
首先要做的是检查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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; } |
现在我们知道,如果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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; } } |
如果此检查失败,那么
下一次检查计算整数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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; } |
另一方面,如果整数
1 2 3 4 5 6 | if (nbits <= 48) { j = PyLong_AsDouble(w); /* It's impossible that <= 48 bits overflowed. */ assert(j != -1.0 || ! PyErr_Occurred()); goto Compare; } |
从这一点开始,我们知道
1 2 3 4 5 6 7 | if (nbits <= 48) { /*"Multiply both sides" by -1; this also swaps the * comparator. */ i = -i; op = _Py_SwappedOp[op]; } |
现在函数查看浮点的指数。回想一下,可以将浮点(忽略符号)写成有效位*2展开式,有效位表示一个介于0.5和1之间的数字:好的。
1 2 3 4 5 6 | (void) frexp(i, &exponent); if (exponent < 0 || (size_t)exponent < nbits) { i = 1.0; j = 2.0; goto Compare; } |
这检查两件事。如果指数小于0,则浮点数小于1(因此其大小小于任何整数)。或者,如果指数小于
如果这两个检查失败,函数将查看指数是否大于
1 2 3 4 5 | if ((size_t)exponent > nbits) { i = 2.0; j = 1.0; goto Compare; } |
如果检查不成功,我们知道浮动EDOCX1的指数(0)与整数
现在可以比较这两个值的唯一方法是从
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | { 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在创建这些新对象时必须做的额外错误检查和垃圾跟踪。不用说,这增加了额外的开销,并解释了为什么问题中突出显示的值比其他值要慢得多。好的。
下面是比较函数执行的检查的摘要。好的。
让
检查
w 是nan 还是inf 。如果是,根据w 的类型分别处理这个特殊情况。好的。如果没有,直接比较
v 和w ,它们的表示是c的两倍。好的。
如果
提取
v 和w 的符号。如果它们不同,那么我们知道v 和w 是不同的,这是更大的值。好的。(符号相同)检查
w 是否有太多的位可以作为浮点(多于size_t )。如果是这样,那么w 的震级大于v 。好的。检查
w 是否有48位或更少的位。如果是这样的话,它就可以安全地被铸造成C双精度,而不会损失精度,并且可以与v 进行比较。好的。(
w 有48位以上。现在,我们将把w 视为一个正整数,适当地更改了比较操作。)好的。考虑float EDOCX1的指数(0)。如果指数为负,那么
v 小于1 ,因此小于任何正整数。否则,如果指数小于w 中的位数,则它必须小于w 。好的。如果
v 的指数大于w 中的位数,则v 大于w 。好的。(指数与
w 中的位数相同。)好的。最终检查。将
v 拆分为整数和小数部分。将整数部分加倍,然后加1以补偿小数部分。现在是整数w 的两倍。将这两个新的整数进行比较以得到结果。好的。
好啊。
使用具有任意精度浮点和整数的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | ~ $ 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 |