关于python:为什么有些float>integer比较比其他的慢四倍?

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时,最坏的情况是:好的。

  • vw的符号相同(均为正或均为负)。
  • 整数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实现的。好的。更详细的比较

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

下面是该函数执行的检查的逐步说明。当试图理解函数的作用时,python源代码中的注释实际上非常有用,所以我将它们放在相关的地方。我还将这些检查总结在答案底部的列表中。好的。

主要的思想是将python对象vw映射到两个适当的C双精度,ij,然后可以很容易地进行比较,得出正确的结果。python 2和python 3都使用相同的思想来实现这一点(前者只分别处理intlong类型)。好的。

首先要做的是检查v是否确实是python float,并将其映射到c double i。接下来,函数检查w是否也是一个浮点数,并将其映射到一个c双j上。这是该函数的最佳情况,因为可以跳过所有其他检查。该函数还检查vinf还是nan:好的。

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;
    }

现在我们知道,如果w未通过这些检查,它就不是python float。现在函数检查它是否是一个python整数。如果是这种情况,最简单的测试是提取v的符号和w的符号(如果为零,返回0,如果为负,返回-1,如果为正,返回1。如果符号不同,这是返回比较结果所需的所有信息:好的。

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;
        }
    }

如果此检查失败,那么vw具有相同的符号。好的。

下一次检查计算整数w中的位数。如果它有太多的位,那么它就不可能被保持为浮点,因此它的大小必须大于浮点v:好的。

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;
    }

另一方面,如果整数w有48位或更少,它可以安全地转入c双j并比较:好的。

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;
    }

从这一点开始,我们知道w有49个或更多的位。将w视为正整数比较方便,必要时更改符号和比较运算符:好的。

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(因此其大小小于任何整数)。或者,如果指数小于w中的位数,那么我们得到v < |w|,因为有效值*2exponent小于2nbits。好的。

如果这两个检查失败,函数将查看指数是否大于w中的位数。由此可见,*2的显著性大于2nbits,因此v > |w|:好的。

1
2
3
4
5
    if ((size_t)exponent > nbits) {
        i = 2.0;
        j = 1.0;
        goto Compare;
    }

如果检查不成功,我们知道浮动EDOCX1的指数(0)与整数w中的位数相同。好的。

现在可以比较这两个值的唯一方法是从vw构造两个新的python整数。其思想是丢弃EDOCX1的小数部分(0),将整数部分翻倍,然后添加一个。w也加倍了,这两个新的python对象可以进行比较,以给出正确的返回值。使用一个小值的例子,通过比较EDOCX1(返回false)来确定4.65 < 4。好的。

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在创建这些新对象时必须做的额外错误检查和垃圾跟踪。不用说,这增加了额外的开销,并解释了为什么问题中突出显示的值比其他值要慢得多。好的。

下面是比较函数执行的检查的摘要。好的。

v成为一个浮点数,并将其铸造为C双精度。现在,如果w也是一个浮动:好的。

  • 检查wnan还是inf。如果是,根据w的类型分别处理这个特殊情况。好的。

  • 如果没有,直接比较vw,它们的表示是c的两倍。好的。

如果w是整数:好的。

  • 提取vw的符号。如果它们不同,那么我们知道vw是不同的,这是更大的值。好的。

  • (符号相同)检查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的两倍。将这两个新的整数进行比较以得到结果。好的。

好啊。


使用具有任意精度浮点和整数的gmpy2,可以获得更均匀的比较性能:

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