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


284

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

例えば:

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


2.7と3.xで同じですか?
thefourtheye

上記のタイミングはPython 3.4からのものです-2.7を実行しているLinuxコンピューターでは、タイミングに同様の不一致がありました(3ビットと4ビットの間の速度が遅い)。
Alex Riley

1
興味深い記事をありがとう。質問のきっかけは何ですか?ランダムにタイミングを比較しただけですか、それとも背後にストーリーがありますか?
Veedrac

3
@Veedrac:ありがとうございます。話はそれほど多くありません。浮動小数点数と整数がどれだけ速く比較され、いくつかの値の時間を計り、小さな違いに気づいたのは、ぼんやりと不思議に思いました。その後、Pythonが浮動小数点数と大きな整数を正確に比較する方法がまったくわからないことに気付きました。私はソースを理解するためにしばらく時間を費やし、最悪のケースが何であるかを学びました。
Alex Riley

2
@YvesDaoust:それらの特定の値ではなく、(それは信じられないほどの幸運だったでしょう!)さまざまな値のペアを試しましたが、タイミングの違いが小さいことに気づきました(たとえば、小さいマグニチュードのフロートを類似の整数と非常に大きい整数と比較した場合)。比較がどのように機能するかを理解するためにソースを見て初めて2 ^ 49ケースについて学びました。最も魅力的な方法でトピックを提示したので、私は質問の値を選択しました。
Alex Riley

回答:


354

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

比較はかなり悪夢です

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

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

float vをinteger / long と比較するw場合、最悪のケースは次のとおりです。

  • v そして w同一の符号(正または両方負の両方)を有します、
  • 整数wは、それを保持できる十分な数のビットを持っていますsize_t型に(通常32ビットまたは64ビット)。
  • 整数 wは少なくとも49ビットです。
  • floatの指数は、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オブジェクトvw2つの適切なCのdouble、iおよびをマップするjことです。これらを簡単に比較して、正しい結果を得ることができます。Python 2とPython 3はどちらも同じアイデアを使用してこれを行います(前者は別々に処理intして入力するだけlongです)。

最初にすることは、それvが間違いなくPython floatであることを確認し、それをC doubleにマップすることiです。次に、関数はがwfloatでもあるかどうかを調べ、それを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フロートではないことがわかりました。次に、関数はそれがPython整数かどうかをチェックします。この場合、最も簡単なテストは、符号vと符号を抽出することですw0ゼロの-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;
    }

この時点から、w49ビット以上あることがわかります。w正の整数として扱うと便利なので、必要に応じて符号と比較演算子を変更します。

    if (nbits <= 48) {
        /* "Multiply both sides" by -1; this also swaps the
         * comparator.
         */
        i = -i;
        op = _Py_SwappedOp[op];
    }

これで、関数は浮動小数点の指数を調べます。floatは(signingを無視して)仮数* 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より小さい(したがって、どの整数よりも大きさが小さい)。または、指数がビット数よりも小さい場合、有効桁数* 2の指数は2 nbitsより小さいため、wそれが得られます。v < |w|

これら2つのチェックに失敗すると、関数は指数がのビット数より大きいかどうかを調べwます。このショーその仮* 2 指数が 2以上であるNBITSのでv > |w|

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

このチェックが成功しなかった場合、浮動小数点の指数がv整数のビット数と同じであることがわかりwます。

2つの値を比較できる唯一の方法は、vおよびから2つの新しいPython整数を作成することwです。の考え方は、の小数部分を破棄し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フロートを可能とダブルCとしてそれをキャスト。さて、wもがフロートである場合:

  • かどうかをチェックしwていますnaninf。その場合、のタイプに応じて、この特殊なケースを個別に処理してくださいw

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

wが整数の場合:

  • vおよびの兆候を抽出しwます。それらが異なる場合、私たちは知ってvおりw、異なるので、どちらがより大きな価値です。

  • 符号は同じです。wが浮動小数点数になるにはビットが多すぎるかどうかを確認します(以上size_t)。もしそうなら、wよりも大きさがありvます。

  • w48ビット以下かどうかを確認します。その場合、その精度を失うことなく、Cのdoubleに安全にキャストでき、と比較できvます。

  • w48ビット以上あります。w比較演算子を適切に変更した正の整数として処理します。

  • floatの指数を考えvます。指数が負である場合には、vより小さく、1したがってより少ない任意の正の整数より。そうでない場合、指数がビット数よりも小さい場合は、wそれよりも小さくなければなりませんw

  • 指数は、IF vのビット数よりも大きい場合w、次にvよりも大きいですw

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

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


4
よくできたPython開発者-ほとんどの言語の実装では、浮動小数点数と整数の比較は正確ではないと言って問題を解決していました。
user253751 2016年

4

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

1
私はまだこのライブラリを使用していませんが、非常に役立つ可能性があります。ありがとう!
Alex Riley

sympyとmpmathで使用されています
denfromufa

CPython decimalは標準ライブラリにもあります
denfromufa
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.