web-dev-qa-db-ja.com

一部のfloat <整数比較が他の比較より4倍遅いのはなぜですか?

浮動小数点数を整数と比較するとき、いくつかの値のペアは、同様の大きさの他の値よりも評価に時間がかかります。

例えば:

>>> 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が他の値よりもいくつかの値のペアのほうが苦労しているように見えるのか、単に興味があります。

282
Alex Riley

FloatオブジェクトのPythonソースコードのコメントは、次のことを認めています。

比較はほとんど悪夢です

これは、フロートと整数を比較する場合に特に当てはまります。フロートとは異なり、Pythonの整数は任意に大きく、常に正確であるためです。整数を浮動小数点数にキャストしようとすると、精度が失われ、比較が不正確になる可能性があります。小数部分が失われるため、フロートを整数にキャストしようとしても機能しません。

この問題を回避するために、Pythonは一連のチェックを実行し、チェックのいずれかが成功すると結果を返します。 2つの値の符号を比較し、整数が「大きすぎて」浮動小数点数でないかどうかを比較し、浮動小数点数の指数を整数の長さと比較します。これらのチェックがすべて失敗した場合、結果を取得するために比較する2つの新しいPythonオブジェクトを作成する必要があります。

Float vを整数/ long 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は、浮動小数点の指数と整数のビット数の両方であることがわかります。両方の数値が正であるため、上記の4つの基準が満たされています。

いずれかの値を大きく(または小さく)すると、整数のビット数または指数の値が変更される可能性があるため、Pythonは比較を実行せずに比較の結果を決定できます高価な最終チェック。

これは、言語のCPython実装に固有です。


より詳細な比較

float_richcompare 関数は、2つの値vwの間の比較を処理します。

以下は、関数が実行するチェックの段階的な説明です。 Pythonソース内のコメントは、関数が何をするのかを理解しようとする際に実際に非常に役立つので、関連する場所にコメントを残しました。また、これらのチェックを回答の下部にあるリストにまとめました。

主なアイデアは、Pythonオブジェクトvおよびwを2つの適切なC double、iおよびjにマッピングすることです。正しい結果を得るために。 Python 2とPython 3は両方とも同じアイデアを使用してこれを行います(前者はintlongタイプを別々に処理します)。

最初に行うことは、vが間違いなくPython floatであることを確認し、C double iにマップすることです。次に、関数はwもfloatであるかどうかを調べ、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 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;
        }
    }   

このチェックに失敗した場合、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 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つの値を比較できる唯一の方法は、vwから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の場合:

  • wnanまたはinfであるかどうかを確認します。その場合、wのタイプに応じて、この特殊なケースを個別に処理します。

  • そうでない場合は、vwをC doubleとしての表現で直接比較します。

wが整数の場合:

  • vおよびwの記号を抽出します。それらが異なる場合、vwが異なり、どちらが大きいかがわかります。

  • 記号は同じです。wのビット数が多すぎて浮動小数点数にならないかどうかを確認します(size_t以上)。その場合、wvよりも大きいです。

  • wのビット数が48以下かどうかを確認します。その場合、精度を失わずにvと比較して安全にCのdoubleにキャストできます。

  • wは48ビット以上です。ここで、wを比較演算子を適切に変更した正の整数として扱います。

  • フロートの指数vを考慮してください。指数が負の場合、v1より小さいので、正の整数より小さいです。それ以外の場合、指数がwのビット数よりも小さい場合、wよりも小さくなければなりません。

  • vの指数がwのビット数より大きい場合、vwより大きいです。

  • 指数はwのビット数と同じです)

  • 最終チェック。 vを整数部分と小数部分に分割します。整数部を2倍にし、1を追加して小数部を補正します。整数wを2倍にします。代わりに、これら2つの新しい整数を比較して結果を取得します。

351
Alex Riley

任意の精度の浮動小数点数と整数で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
4
denfromufa