Tại sao một số float <so sánh số nguyên chậm hơn bốn lần so với số khác?


284

Khi so sánh số float với số nguyên, một số cặp giá trị mất nhiều thời gian hơn để được đánh giá so với các giá trị khác có cường độ tương tự.

Ví dụ:

>>> import timeit
>>> timeit.timeit("562949953420000.7 < 562949953421000") # run 1 million times
0.5387085462592742

Nhưng nếu số float hoặc số nguyên được làm nhỏ hơn hoặc lớn hơn theo một số tiền nhất định, thì phép so sánh sẽ chạy nhanh hơn nhiều:

>>> timeit.timeit("562949953420000.7 < 562949953422000") # integer increased by 1000
0.1481498428446173
>>> timeit.timeit("562949953423001.8 < 562949953421000") # float increased by 3001.1
0.1459577925548956

Thay đổi toán tử so sánh (ví dụ: sử dụng ==hoặc >thay vào đó) không ảnh hưởng đến thời gian theo bất kỳ cách đáng chú ý nào.

Điều này không chỉ liên quan đến độ lớn bởi vì việc chọn các giá trị lớn hơn hoặc nhỏ hơn có thể dẫn đến sự so sánh nhanh hơn, vì vậy tôi nghi ngờ rằng đó là một cách đáng tiếc khi các bit xếp hàng.

Rõ ràng, so sánh các giá trị này là quá đủ nhanh cho hầu hết các trường hợp sử dụng. Tôi chỉ đơn giản là tò mò về lý do tại sao Python dường như đấu tranh nhiều hơn với một số cặp giá trị hơn so với các cặp khác.


Có giống nhau ở cả 2.7 và 3.x không?
thefourtheye

Các định thời gian ở trên là từ Python 3.4 - trên máy tính Linux của tôi chạy 2.7 có sự khác biệt tương tự về thời gian (chậm hơn từ 3 đến 4 lần một bit).
Alex Riley

1
Cảm ơn cho bài viết thú vị. Tôi tò mò về điều gì đã truyền cảm hứng cho câu hỏi - bạn chỉ là những so sánh ngẫu nhiên về thời gian hay có một câu chuyện đằng sau nó?
Veedrac

3
@Veedrac: Cảm ơn bạn. Không có nhiều câu chuyện: Tôi lơ đãng tự hỏi làm thế nào nhanh chóng nổi và số nguyên được so sánh, hẹn giờ một vài giá trị và nhận thấy một số khác biệt nhỏ. Sau đó, tôi nhận ra rằng tôi hoàn toàn không biết làm thế nào Python quản lý để so sánh chính xác số float và số nguyên lớn. Tôi đã dành một thời gian cố gắng để hiểu nguồn và tìm hiểu trường hợp xấu nhất là gì.
Alex Riley

2
@YvesDaoust: không phải những giá trị cụ thể đó, không (đó sẽ là may mắn đáng kinh ngạc!). Tôi đã thử nhiều cặp giá trị khác nhau và nhận thấy sự khác biệt nhỏ hơn về thời gian (ví dụ: so sánh độ nổi có độ lớn nhỏ với số nguyên tương tự so với số nguyên rất lớn). Tôi đã tìm hiểu về trường hợp 2 ^ 49 chỉ sau khi xem nguồn để hiểu cách so sánh hoạt động. Tôi chọn các giá trị trong câu hỏi vì chúng trình bày chủ đề theo cách hấp dẫn nhất.
Alex Riley

Câu trả lời:


354

Một nhận xét trong mã nguồn Python cho các đối tượng float thừa nhận rằng:

So sánh là một cơn ác mộng

Điều này đặc biệt đúng khi so sánh số float với một số nguyên, bởi vì, không giống như số float, số nguyên trong Python có thể lớn tùy ý và luôn chính xác. Cố gắng truyền số nguyên cho một số float có thể làm mất độ chính xác và làm cho phép so sánh không chính xác. Cố gắng chuyển float sang một số nguyên cũng sẽ không hoạt động vì bất kỳ phần phân số nào cũng sẽ bị mất.

Để giải quyết vấn đề này, Python thực hiện một loạt các kiểm tra, trả về kết quả nếu một trong các kiểm tra thành công. Nó so sánh các dấu hiệu của hai giá trị, sau đó liệu số nguyên có "quá lớn" là một số float hay không, sau đó so sánh số mũ của số float với chiều dài của số nguyên. Nếu tất cả các kiểm tra này không thành công, cần phải xây dựng hai đối tượng Python mới để so sánh để có được kết quả.

Khi so sánh số float vvới số nguyên / dài w, trường hợp xấu nhất là:

  • vwcó cùng dấu (cả dương hoặc cả âm),
  • số nguyên wcó ít bit đủ để có thể được giữ trong size_tloại (thường là 32 hoặc 64 bit),
  • số nguyên wcó ít nhất 49 bit,
  • số mũ của float vgiống như số bit trong w.

Và đây chính xác là những gì chúng ta có cho các giá trị trong câu hỏi:

>>> import math
>>> math.frexp(562949953420000.7) # gives the float's (significand, exponent) pair
(0.9999999999976706, 49)
>>> (562949953421000).bit_length()
49

Chúng ta thấy 49 là cả số mũ của số float và số bit trong số nguyên. Cả hai số đều dương và vì vậy bốn tiêu chí trên được đáp ứng.

Việc chọn một trong các giá trị lớn hơn (hoặc nhỏ hơn) có thể thay đổi số bit của số nguyên hoặc giá trị của số mũ và do đó Python có thể xác định kết quả so sánh mà không cần thực hiện kiểm tra cuối cùng đắt tiền.

Điều này là cụ thể cho việc thực hiện CPython của ngôn ngữ.


Sự so sánh chi tiết hơn

Các float_richcomparechức năng xử lý sự so sánh giữa hai giá trị vw.

Dưới đây là mô tả từng bước của các kiểm tra mà chức năng thực hiện. Các ý kiến ​​trong nguồn Python thực sự rất hữu ích khi cố gắng hiểu chức năng này làm gì, vì vậy tôi đã để chúng ở nơi có liên quan. Tôi cũng đã tóm tắt những kiểm tra này trong một danh sách dưới chân câu trả lời.

Ý tưởng chính là ánh xạ các đối tượng Python vwhai nhân đôi C thích hợp, ijsau đó có thể dễ dàng so sánh để đưa ra kết quả chính xác. Cả Python 2 và Python 3 đều sử dụng cùng một ý tưởng để thực hiện điều này (trước đây chỉ xử lý intlonggõ riêng).

Điều đầu tiên cần làm là kiểm tra xem đó có phải vlà một float Python và ánh xạ nó tới C double i. Tiếp theo, hàm xem xét xem có phải wlà số float hay không và ánh xạ nó tới C double j. Đây là trường hợp tốt nhất cho chức năng vì tất cả các kiểm tra khác có thể được bỏ qua. Kiểm tra chức năng cũng để xem liệu vinfhay 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;
    }

Bây giờ chúng tôi biết rằng nếu w thất bại các kiểm tra này, nó không phải là một float Python. Bây giờ hàm kiểm tra xem đó có phải là số nguyên Python không. Nếu đây là trường hợp, thử nghiệm đơn giản nhất là trích xuất dấu hiệu vvà dấu hiệu của w(trả về 0nếu bằng 0, -1nếu âm, 1nếu dương). Nếu các dấu hiệu khác nhau, đây là tất cả thông tin cần thiết để trả về kết quả so sánh:

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

Nếu kiểm tra này thất bại, sau đó vwcó cùng một dấu hiệu.

Kiểm tra tiếp theo đếm số bit trong số nguyên w. Nếu nó có quá nhiều bit thì nó không thể được giữ như một float và do đó phải có độ lớn lớn hơn float 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;
    }

Mặt khác, nếu số nguyên wcó 48 bit trở xuống, nó có thể biến thành một C một cách an toàn jvà so sánh:

    if (nbits <= 48) {
        j = PyLong_AsDouble(w);
        /* It's impossible that <= 48 bits overflowed. */
        assert(j != -1.0 || ! PyErr_Occurred());
        goto Compare;
    }

Từ thời điểm này trở đi, chúng ta biết rằng w có 49 bit trở lên. Sẽ thuận tiện khi coi wlà số nguyên dương, vì vậy hãy thay đổi dấu và toán tử so sánh khi cần thiết:

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

Bây giờ chức năng nhìn vào số mũ của phao. Hãy nhớ lại rằng một dấu phẩy có thể được viết (bỏ qua dấu hiệu) là có ý nghĩa * 2 số mũ và ý nghĩa đó đại diện cho một số trong khoảng từ 0,5 đến 1:

    (void) frexp(i, &exponent);
    if (exponent < 0 || (size_t)exponent < nbits) {
        i = 1.0;
        j = 2.0;
        goto Compare;
    }

Điều này kiểm tra hai điều. Nếu số mũ nhỏ hơn 0 thì số float nhỏ hơn 1 (và vì vậy độ lớn nhỏ hơn bất kỳ số nguyên nào). Hoặc, nếu số mũ nhỏ hơn số bit trong wđó thì chúng ta có số đó v < |w|kể từ khi có ý nghĩa * 2 số mũ vì số mũ nhỏ hơn 2 nbits .

Không thực hiện hai kiểm tra này, hàm sẽ xem liệu số mũ có lớn hơn số bit trong hay không w. Điều này cho thấy số mũ có ý nghĩa * 2 lớn hơn 2 nbits và do đóv > |w| :

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

Nếu kiểm tra này không thành công, chúng tôi biết rằng số mũ của số float v giống như số bit trong số nguyên w.

Cách duy nhất mà hai giá trị có thể được so sánh bây giờ là xây dựng hai số nguyên Python mới từ vw. Ý tưởng là loại bỏ phần phân số của v, nhân đôi phần nguyên và sau đó thêm một phần. wcũng được nhân đôi và hai đối tượng Python mới này có thể được so sánh để đưa ra giá trị trả về chính xác. Sử dụng một ví dụ với các giá trị nhỏ, 4.65 < 4sẽ được xác định bằng cách so sánh (2*4)+1 == 9 < 8 == (2*4)(trả về 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
    }
}

Để cho ngắn gọn, tôi đã bỏ qua Python kiểm tra lỗi và theo dõi rác bổ sung phải làm khi nó tạo ra các đối tượng mới này. Không cần phải nói, điều này bổ sung thêm chi phí và giải thích tại sao các giá trị được tô sáng trong câu hỏi chậm hơn đáng kể so với các giá trị khác.


Dưới đây là một bản tóm tắt các kiểm tra được thực hiện bởi chức năng so sánh.

Hãy vlà một cái phao và đúc nó thành một đôi C. Bây giờ nếuw cũng là một float:

  • Kiểm tra xem wnanhay inf. Nếu vậy, xử lý riêng trường hợp đặc biệt này tùy thuộc vào loại w.

  • Nếu không, so sánh vwtrực tiếp bằng cách biểu diễn của chúng khi C nhân đôi.

Nếu wlà một số nguyên:

  • Trích xuất các dấu hiệu của vw. Nếu chúng khác nhau thì chúng ta biết vwkhác biệt và đó là giá trị lớn hơn.

  • ( Các dấu hiệu giống nhau. ) Kiểm tra xem wcó quá nhiều bit để làm nổi hay không (nhiều hơn size_t). Nếu vậy, wcó cường độ lớn hơn v.

  • Kiểm tra nếu wcó 48 bit hoặc ít hơn. Nếu vậy, nó có thể được đúc một cách an toàn đến gấp đôi C mà không mất độ chính xác và so vớiv .

  • ( wcó hơn 48 bit. Bây giờ chúng ta sẽ coi wlà một số nguyên dương đã thay đổi op so sánh cho phù hợp. )

  • Hãy xem xét số mũ của phao v. Nếu số mũ là âm, thì vnhỏ hơn 1và do đó nhỏ hơn bất kỳ số nguyên dương nào. Khác, nếu số mũ nhỏ hơn số bit trong wthì nó phải nhỏ hơn w.

  • Nếu số mũ của vlớn hơn số bit trong wthì vlớn hơn w.

  • ( Số mũ giống như số bit trong w. )

  • Kiểm tra cuối cùng. Chia vthành các phần nguyên và phần của nó. Nhân đôi phần nguyên và thêm 1 để bù cho phần phân số. Bây giờ nhân đôi số nguyên w. Thay vào đó, so sánh hai số nguyên mới này để có kết quả.


4
Các nhà phát triển Python hoàn thành tốt - hầu hết các triển khai ngôn ngữ sẽ xử lý vấn đề bằng cách nói các phép so sánh float / số nguyên là không chính xác.
dùng253751

4

Sử dụng gmpy2với số phao và số nguyên chính xác tùy ý, có thể có được hiệu suất so sánh đồng đều hơn:

~ $ 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
Tôi chưa sử dụng thư viện này, nhưng nó có vẻ rất hữu ích. Cảm ơn!
Alex Riley

Nó được sử dụng bởi sympy và mpmath
denfromufa

CPython cũng có decimal trong thư viện tiêu chuẩn
denfromufa
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.