【问题标题】:Why are some float < integer comparisons four times slower than others?为什么某些浮点 < 整数比较比其他的慢四倍?
【发布时间】:2015-07-18 00:08:25
【问题描述】:

在将浮点数与整数进行比较时,某些值对的计算时间比其他类似大小的值要长得多。

例如:

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

更改比较运算符(例如,改用==&gt;)不会以任何明显的方式影响时间。

这与幅度无关单独,因为选择更大或更小的值可以导致更快的比较,所以我怀疑这归结为位排列的一些不幸方式。

显然,对于大多数用例来说,比较这些值已经足够快了。我只是很好奇为什么 Python 似乎在某些值对上比在其他值上更挣扎。

【问题讨论】:

  • 2.7 和 3.x 都一样吗?
  • 以上时间来自 Python 3.4 - 在我运行 2.7 的 Linux 计算机上存在类似的时间差异(慢 3 到 4 倍)。
  • 感谢有趣的文章。我很好奇是什么激发了这个问题 - 你只是随机时间比较还是背后有故事?
  • @Veedrac:谢谢。故事不多:我心不在焉地想知道浮点数和整数的比较速度有多快,对几个值进行计时,并注意到一些细微的差异。然后我意识到我完全不知道 Python 是如何准确地比较浮点数和大整数的。我花了一段时间试图了解来源并了解最坏的情况是什么。
  • @YvesDaoust:不是那些特定的值,不(那将是不可思议的运气!)。我尝试了各种值对,并注意到时间上的差异较小(例如,将小幅度的浮点数与相似的整数与非常大的整数进行比较)。在查看源代码以了解比较的工作原理后,我才了解了 2^49 案例。我选择了问题中的值,因为它们以最引人注目的方式呈现了主题。

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


【解决方案1】:

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

【讨论】:

  • 我还没有使用过这个库,但它看起来很有用。谢谢!
  • sympy 和 mpmath 使用
  • CPython 在标准库中也有decimal
【解决方案2】:

浮点对象的 Python 源代码中的注释承认:

Comparison is pretty much a nightmare

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

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

当比较一个浮点数 v 和一个整数/长整数 w 时,最坏的情况是:

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

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

>>> 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 源代码中的 cmets 在尝试理解函数的作用时实际上非常有用,因此我将它们留在了相关的地方。我还在答案底部的列表中总结了这些检查。

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

首先要做的是检查v 绝对是一个Python 浮点数并将它映射到一个C 双精度i。接下来,该函数查看w 是否也是浮点数并将其映射到C 双精度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)。如果符号不同,这就是返回比较结果所需的全部信息:

    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:

    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 并进行比较:

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

现在函数查看浮点数的指数。回想一下,浮点数可以写(忽略符号)为有效数 * 2exponent,并且有效数表示 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 &lt; |w|,因为有效位 * 2exponent 小于 2nbits

如果这两项检查失败,函数会查看指数是否大于w 中的位数。这表明有效数 * 2exponent 大于 2nbitsv &gt; |w|:

    if ((size_t)exponent > nbits) {
        i = 2.0;
        j = 1.0;
        goto Compare;
    }

如果此检查不成功,我们知道浮点 v 的指数与整数 w 中的位数相同。

现在可以比较这两个值的唯一方法是从 vw 构造两个新的 Python 整数。这个想法是丢弃v的小数部分,将整数部分加倍,然后加一。 w 也加倍,这两个新的 Python 对象可以进行比较以给出正确的返回值。使用具有小值的示例,4.65 &lt; 4 将通过比较 (2*4)+1 == 9 &lt; 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 在创建这些新对象时必须执行的额外错误检查和垃圾跟踪。不用说,这增加了额外的开销,并解释了为什么问题中突出显示的值比其他值要慢得多。


这里是比较函数执行的检查的摘要。

v 为浮点数并将其转换为 C 双精度数。现在,如果w 也是一个浮点数:

  • 检查wnan还是inf。如果是,请根据w的类型单独处理这种特殊情况。

  • 如果不是,请直接比较 vw 作为 C 双精度的表示形式。

如果w 是整数:

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

  • (符号相同。) 检查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 加倍。而是比较这两个新整数以获得结果。

【讨论】:

  • Python 开发人员做得很好 - 大多数语言实现都会通过说浮点/整数比较不准确来解决这个问题。
猜你喜欢
  • 2020-09-15
  • 1970-01-01
  • 2016-11-10
  • 2015-01-20
  • 1970-01-01
  • 2015-02-20
  • 2011-03-18
  • 1970-01-01
相关资源
最近更新 更多