浮動小数点数を整数と比較するとき、いくつかの値のペアは、同様の大きさの他の値よりも評価に時間がかかります。
例えば:
>>> 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が他の値よりもいくつかの値のペアのほうが苦労しているように見えるのか、単に興味があります。
FloatオブジェクトのPythonソースコードのコメントは、次のことを認めています。
これは、フロートと整数を比較する場合に特に当てはまります。フロートとは異なり、Pythonの整数は任意に大きく、常に正確であるためです。整数を浮動小数点数にキャストしようとすると、精度が失われ、比較が不正確になる可能性があります。小数部分が失われるため、フロートを整数にキャストしようとしても機能しません。
この問題を回避するために、Pythonは一連のチェックを実行し、チェックのいずれかが成功すると結果を返します。 2つの値の符号を比較し、整数が「大きすぎて」浮動小数点数でないかどうかを比較し、浮動小数点数の指数を整数の長さと比較します。これらのチェックがすべて失敗した場合、結果を取得するために比較する2つの新しいPythonオブジェクトを作成する必要があります。
Float v
を整数/ 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は、浮動小数点の指数と整数のビット数の両方であることがわかります。両方の数値が正であるため、上記の4つの基準が満たされています。
いずれかの値を大きく(または小さく)すると、整数のビット数または指数の値が変更される可能性があるため、Pythonは比較を実行せずに比較の結果を決定できます高価な最終チェック。
これは、言語のCPython実装に固有です。
float_richcompare
関数は、2つの値v
とw
の間の比較を処理します。
以下は、関数が実行するチェックの段階的な説明です。 Pythonソース内のコメントは、関数が何をするのかを理解しようとする際に実際に非常に役立つので、関連する場所にコメントを残しました。また、これらのチェックを回答の下部にあるリストにまとめました。
主なアイデアは、Pythonオブジェクトv
およびw
を2つの適切なC double、i
およびj
にマッピングすることです。正しい結果を得るために。 Python 2とPython 3は両方とも同じアイデアを使用してこれを行います(前者はint
とlong
タイプを別々に処理します)。
最初に行うことは、v
が間違いなくPython floatであることを確認し、C double i
にマップすることです。次に、関数はw
もfloatであるかどうかを調べ、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 floatではないことがわかります。これで、関数は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;
}
}
このチェックに失敗した場合、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];
}
ここで、関数は浮動小数点の指数を調べます。浮動小数点数は仮数として記述できます(符号を無視)* 2指数 また、仮数は0.5から1の間の数を表します。
(void) frexp(i, &exponent);
if (exponent < 0 || (size_t)exponent < nbits) {
i = 1.0;
j = 2.0;
goto Compare;
}
これは2つのことを確認します。指数が0未満の場合、フロートは1より小さい(したがって、整数よりも大きさが小さい)。または、指数がw
のビット数よりも小さい場合は、仮数* 2からv < |w|
になります指数 2未満nbits。
これら2つのチェックに失敗すると、関数は指数がw
のビット数より大きいかどうかを確認します。これは、その仮数* 2を示しています指数 2より大きいnbits そして、v > |w|
:
if ((size_t)exponent > nbits) {
i = 2.0;
j = 1.0;
goto Compare;
}
このチェックが成功しなかった場合、float v
の指数が整数w
のビット数と同じであることがわかります。
2つの値を比較できる唯一の方法は、v
とw
から2つの新しいPython整数を作成することです。アイデアは、v
の小数部分を破棄し、整数部分を2倍にしてから1を追加することです。 w
も2倍になり、これら2つの新しいPythonオブジェクトを比較して、正しい戻り値を得ることができます。小さな値の例を使用すると、4.65 < 4
は比較(2*4)+1 == 9 < 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
をfloatにして、C doubleとしてキャストします。 w
もfloatの場合:
w
がnan
またはinf
であるかどうかを確認します。その場合、w
のタイプに応じて、この特殊なケースを個別に処理します。
そうでない場合は、v
とw
をC doubleとしての表現で直接比較します。
w
が整数の場合:
v
およびw
の記号を抽出します。それらが異なる場合、v
とw
が異なり、どちらが大きいかがわかります。
(記号は同じです。)w
のビット数が多すぎて浮動小数点数にならないかどうかを確認します(size_t
以上)。その場合、w
はv
よりも大きいです。
w
のビット数が48以下かどうかを確認します。その場合、精度を失わずにv
と比較して安全にCのdoubleにキャストできます。
(w
は48ビット以上です。ここで、w
を比較演算子を適切に変更した正の整数として扱います。)
フロートの指数v
を考慮してください。指数が負の場合、v
は1
より小さいので、正の整数より小さいです。それ以外の場合、指数がw
のビット数よりも小さい場合、w
よりも小さくなければなりません。
v
の指数がw
のビット数より大きい場合、v
はw
より大きいです。
(指数はw
。のビット数と同じです)
最終チェック。 v
を整数部分と小数部分に分割します。整数部を2倍にし、1を追加して小数部を補正します。整数w
を2倍にします。代わりに、これら2つの新しい整数を比較して結果を取得します。
任意の精度の浮動小数点数と整数で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