为什么有些 float < integer 比较比其他比较慢四倍?
Why are some float < integer comparisons four times slower than others?
将浮点数与整数进行比较时,某些值对的计算时间比类似大小的其他值要长得多。
例如:
>>> 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 似乎对某些价值观比对其他价值观更加挣扎。
Python 浮动对象源代码中的一条评论承认:
将浮点数与整数进行比较时尤其如此,因为与浮点数不同,Python 中的整数可以任意大并且总是精确的。尝试将整数转换为浮点数可能会失去精度并使比较不准确。尝试将浮点数转换为整数也不会起作用,因为任何小数部分都会丢失。
为了解决这个问题,Python 执行了一系列检查,如果其中一个检查成功,return 将检查结果。它比较两个值的符号,然后整数是否 "too big" 是一个浮点数,然后比较浮点数的指数和整数的长度。如果这些检查都失败了,就需要构造两个新的Python对象进行比较,以获得结果。
比较浮点数 v
和 integer/long w
时,最坏的情况是:
v
和w
符号相同(均为正或均为负),
- 整数
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
函数处理两个值 v
和 w
之间的比较。
下面是函数执行的检查的分步说明。 Python 源代码中的注释在试图理解该函数的作用时实际上非常有帮助,因此我将它们留在了相关的地方。我还在答案底部的列表中总结了这些检查。
主要思想是将Python对象v
和w
映射到两个适当的C双打,i
和j
,然后可以很容易比较给出正确的结果。 Python 2 和 Python 3 使用相同的思路来做到这一点(前者只是分别处理 int
和 long
类型)。
首先要做的是检查 v
是否确实是 Python 浮点数并将其映射到 C double i
。接下来该函数查看 w
是否也是一个浮点数并将其映射到 C double j
。这是该函数的最佳情况,因为可以跳过所有其他检查。该函数还检查 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 整数。如果是这种情况,最简单的测试是提取 v
的符号和 w
的符号(return 0
如果为零,-1
如果为负, 1
如果为正)。如果符号不同,这就是 return 比较结果所需的全部信息:
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 位或更少,它可以安全地转入 C double 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 < |w|
因为有效数 * 2exponent 小于 2nbits。
如果这两个检查失败,该函数将查看指数是否大于 w
中的位数。这表明 significand * 2exponent 大于 2nbits 等 v > |w|
:
if ((size_t)exponent > nbits) {
i = 2.0;
j = 1.0;
goto Compare;
}
如果此检查不成功,我们知道浮点数 v
的指数与整数 w
中的位数相同。
现在可以比较这两个值的唯一方法是从 v
和 w
构造两个新的 Python 整数。思路是舍弃v
的小数部分,将整数部分加倍,然后加一。 w
也加倍,可以比较这两个新的 Python 对象以给出正确的 return 值。使用具有小值的示例,4.65 < 4
将由比较 (2*4)+1 == 9 < 8 == (2*4)
(returning 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 double。现在,如果 w
也是一个浮点数:
检查w
是nan
还是inf
。如果是,则根据 w
.
的类型分别处理这种特殊情况
如果不是,直接比较 v
和 w
作为 C double 的表示。
如果w
是一个整数:
提取v
和w
的符号。如果它们不同,那么我们就知道 v
和 w
是不同的,哪个值更大。
(符号相同) 检查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
加倍。比较这两个新整数而不是得到结果。
使用具有任意精度浮点数和整数的 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
将浮点数与整数进行比较时,某些值对的计算时间比类似大小的其他值要长得多。
例如:
>>> 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 似乎对某些价值观比对其他价值观更加挣扎。
Python 浮动对象源代码中的一条评论承认:
将浮点数与整数进行比较时尤其如此,因为与浮点数不同,Python 中的整数可以任意大并且总是精确的。尝试将整数转换为浮点数可能会失去精度并使比较不准确。尝试将浮点数转换为整数也不会起作用,因为任何小数部分都会丢失。
为了解决这个问题,Python 执行了一系列检查,如果其中一个检查成功,return 将检查结果。它比较两个值的符号,然后整数是否 "too big" 是一个浮点数,然后比较浮点数的指数和整数的长度。如果这些检查都失败了,就需要构造两个新的Python对象进行比较,以获得结果。
比较浮点数 v
和 integer/long w
时,最坏的情况是:
v
和w
符号相同(均为正或均为负),- 整数
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
函数处理两个值 v
和 w
之间的比较。
下面是函数执行的检查的分步说明。 Python 源代码中的注释在试图理解该函数的作用时实际上非常有帮助,因此我将它们留在了相关的地方。我还在答案底部的列表中总结了这些检查。
主要思想是将Python对象v
和w
映射到两个适当的C双打,i
和j
,然后可以很容易比较给出正确的结果。 Python 2 和 Python 3 使用相同的思路来做到这一点(前者只是分别处理 int
和 long
类型)。
首先要做的是检查 v
是否确实是 Python 浮点数并将其映射到 C double i
。接下来该函数查看 w
是否也是一个浮点数并将其映射到 C double j
。这是该函数的最佳情况,因为可以跳过所有其他检查。该函数还检查 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 整数。如果是这种情况,最简单的测试是提取 v
的符号和 w
的符号(return 0
如果为零,-1
如果为负, 1
如果为正)。如果符号不同,这就是 return 比较结果所需的全部信息:
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 位或更少,它可以安全地转入 C double 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 < |w|
因为有效数 * 2exponent 小于 2nbits。
如果这两个检查失败,该函数将查看指数是否大于 w
中的位数。这表明 significand * 2exponent 大于 2nbits 等 v > |w|
:
if ((size_t)exponent > nbits) {
i = 2.0;
j = 1.0;
goto Compare;
}
如果此检查不成功,我们知道浮点数 v
的指数与整数 w
中的位数相同。
现在可以比较这两个值的唯一方法是从 v
和 w
构造两个新的 Python 整数。思路是舍弃v
的小数部分,将整数部分加倍,然后加一。 w
也加倍,可以比较这两个新的 Python 对象以给出正确的 return 值。使用具有小值的示例,4.65 < 4
将由比较 (2*4)+1 == 9 < 8 == (2*4)
(returning 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 double。现在,如果 w
也是一个浮点数:
检查
w
是nan
还是inf
。如果是,则根据w
. 的类型分别处理这种特殊情况
如果不是,直接比较
v
和w
作为 C double 的表示。
如果w
是一个整数:
提取
v
和w
的符号。如果它们不同,那么我们就知道v
和w
是不同的,哪个值更大。(符号相同) 检查
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
加倍。比较这两个新整数而不是得到结果。
使用具有任意精度浮点数和整数的 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