为什么有些浮动<整数比较比其他四倍慢?

时间:2015-05-07 12:11:09

标签: python performance floating-point cpython python-internals

将浮点数与整数进行比较时,某些值对的评估时间比其他类似值的值要长得多。

例如:

>>> 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 个答案:

答案 0 :(得分:350)

float对象的Python源代码中的注释确认:

  

Comparison is pretty much a nightmare

在将float与整数进行比较时尤其如此,因为与浮点数不同,Python中的整数可以任意大并且总是精确的。尝试将整数转换为浮点数可能会失去精度并使比较不准确。试图将浮点数转换为整数也不会起作用,因为任何小数部分都将丢失。

要解决此问题,Python会执行一系列检查,如果其中一项检查成功,则返回结果。它比较了两个值的符号,然后是整数是否太大&#34;要成为一个浮点数,然后将浮点数的指数与整数的长度进行比较。如果所有这些检查都失败,则需要构造两个新的Python对象进行比较以获得结果。

将float v与整数/ long w进行比较时,最糟糕的情况是:

  • vw具有相同的符号(正面或均为负面),
  • 整数w的位数足够少,可以保存在size_t类型中(通常为32或64位),
  • 整数w至少有49位,
  • float v的指数与w中的位数相同。

这正是我们对问题中的价值观所做的:

>>> import math
>>> math.frexp(562949953420000.7) # gives the float's (significand, exponent) pair
(0.9999999999976706, 49)
>>> (562949953421000).bit_length()
49

我们看到49既是float的指数,也是整数中的位数。这两个数字都是正数,因此符合上述四个标准。

选择其中一个值更大(或更小)可以改变整数的位数或指数的值,因此Python能够确定比较的结果而无需执行昂贵的最终检查

这特定于CPython语言的实现。

更详细的比较

float_richcompare函数处理两个值vw之间的比较。

以下是该功能执行的检查的逐步说明。在尝试理解函数的功能时,Python源代码中的注释实际上非常有用,因此我将它们放在相关的位置。我还在答案的最后列表中总结了这些检查。

主要思想是将Python对象vw映射到两个适当的C双精度,ij,然后可以轻松比较它们正确的结果。 Python 2和Python 3都使用相同的想法(前者只分别处理intlong类型)。

要做的第一件事是检查v绝对是一个Python float并将其映射到C double i。接下来,该函数查看w是否也是一个浮点数并将其映射到C double j。这是函数的最佳情况,因为可以跳过所有其他检查。该函数还会检查vinf还是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整数。如果是这种情况,最简单的测试是提取v的符号和w的符号(如果为零,则返回0,如果为负,则返回-1,{{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; } 有48个或更少的位,它可以安全地输入C double w并进行比较:

j

从这一点开始,我们知道 if (nbits <= 48) { j = PyLong_AsDouble(w); /* It's impossible that <= 48 bits overflowed. */ assert(j != -1.0 || ! PyErr_Occurred()); goto Compare; } 有49位或更多位。将w视为正整数会很方便,因此根据需要更改符号和比较运算符:

w

现在该函数查看float的指数。回想一下,浮点数可以写成(忽略符号)为有效数字* 2 指数,并且有效数字表示0.5和1之间的数字:

    if (nbits <= 48) {
        /* "Multiply both sides" by -1; this also swaps the
         * comparator.
         */
        i = -i;
        op = _Py_SwappedOp[op];
    }

这会检查两件事。如果指数小于0,则浮点数小于1(并且其幅度小于任何整数)。或者,如果指数小于 (void) frexp(i, &exponent); if (exponent < 0 || (size_t)exponent < nbits) { i = 1.0; j = 2.0; goto Compare; } 中的位数,那么我们就有w因为有效数* 2 指数小于2 nbits

如果两次检查失败,该函数会查看指数是否大于v < |w|中的位数。这表明有效数* 2 指数大于2 nbits ,因此w

v > |w|

如果此检查未成功,我们知道float if ((size_t)exponent > nbits) { i = 2.0; j = 1.0; goto Compare; } 的指数与整数v中的位数相同。

现在可以比较这两个值的唯一方法是从wv构造两个新的Python整数。想法是丢弃w的小数部分,将整数部分加倍,然后加1。 v也加倍,可以比较这两个新的Python对象,以提供正确的返回值。使用值较小的示例,w将由比较4.65 < 4确定(返回false)。

(2*4)+1 == 9 < 8 == (2*4)

为简洁起见,我忽略了Python在创建这些新对象时必须执行的额外错误检查和垃圾跟踪。毋庸置疑,这会增加额外的开销,并解释为什么问题中突出显示的值比其他值要慢得多。

以下是比较功能执行的检查的摘要。

{ 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 } } 成为一个浮点数并将其转换为C double。现在,如果v也是浮点数:

  • 检查ww还是nan。如果是这样,请根据inf的类型单独处理此特殊情况。

  • 如果没有,请将wv直接比作其C代表的表示。

如果w是整数:

  • 提取wv的标志。如果它们不同,那么我们知道wv是不同的,哪个值更大。

  • 符号相同。)检查w是否有太多位成为浮点数(超过w)。如果是这样,size_t的幅度大于w

  • 检查v是否有48个或更少的位。如果是这样,它可以安全地转换为C double而不会失去其精度,并与w进行比较。

  • v超过48位。我们现在将w视为正整数,并根据需要更改了比较操作。

  • 考虑浮点w的指数。如果指数为负,则v小于v,因此小于任何正整数。否则,如果指数小于1中的位数,则它必须小于w

  • 如果w的指数大于v中的位数,则w大于v

  • 指数与w中的位数相同。

  • 最后检查。将w拆分为整数和小数部分。将整数部分加倍并加1以补偿小数部分。现在将整数v加倍。比较这两个新的整数来获得结果。

答案 1 :(得分:4)

使用具有任意精度浮点数和整数的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