ทำไมบางลอย <เปรียบเทียบจำนวนเต็มสี่ครั้งช้ากว่าคนอื่น ๆ ?


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 - บนคอมพิวเตอร์ Linux ที่ใช้ 2.7 มีความคลาดเคลื่อนที่คล้ายกันในการกำหนดเวลา (ระหว่างเวลา 3 ถึง 4 และช้าลงเล็กน้อย)
Alex Riley

1
ขอบคุณสำหรับบทความที่น่าสนใจ ฉันอยากรู้ว่าสิ่งที่เป็นแรงบันดาลใจคำถาม - คุณเพียงแค่การสุ่มเวลาเปรียบเทียบหรือมีเรื่องราวอยู่เบื้องหลังหรือไม่
Veedrac

3
@Veedrac: ขอบคุณ มีไม่มากของเรื่อง: ฉัน์อย่างเลื่อนลอยเลื่อนลอยสงสัยวิธีการอย่างรวดเร็วและลอยจำนวนเต็มถูกเมื่อเทียบกับค่าหมดเวลาไม่กี่และสังเกตเห็นความแตกต่าง smallish บาง จากนั้นฉันก็รู้ว่าฉันไม่รู้ว่า Python จัดการเปรียบเทียบลอยและจำนวนเต็มอย่างแม่นยำได้อย่างไร ฉันใช้เวลาซักพักเพื่อทำความเข้าใจกับแหล่งที่มาและเรียนรู้ว่าสิ่งที่แย่ที่สุดคืออะไร
Alex Riley

2
@ YvesDaoust: ไม่ใช่ค่าเฉพาะเหล่านั้นไม่ใช่ (นั่นน่าจะเป็นโชคที่เหลือเชื่อ!) ฉันลองใช้ค่าต่าง ๆ และสังเกตเห็นความแตกต่างในเวลาที่กำหนดน้อยลง (เช่นการเปรียบเทียบโฟลว์ขนาดเล็กที่มีจำนวนเต็มคล้ายกับจำนวนเต็มที่มีขนาดใหญ่มาก) ฉันเรียนรู้เกี่ยวกับเคส 2 ^ 49 หลังจากดูที่แหล่งข้อมูลเพื่อทำความเข้าใจว่าการเปรียบเทียบทำงานอย่างไร ผมเลือกค่าในคำถามเพราะพวกเขานำเสนอหัวข้อในทางที่น่าสนใจที่สุด
Alex Riley

คำตอบ:


354

ความคิดเห็นในซอร์สโค้ดของ Python สำหรับวัตถุลอยยอมรับว่า:

การเปรียบเทียบค่อนข้างเป็นฝันร้าย

นี่เป็นความจริงโดยเฉพาะอย่างยิ่งเมื่อเปรียบเทียบการลอยกับจำนวนเต็มเพราะต่างจากการลอยจำนวนเต็มใน Python สามารถใหญ่โดยพลการและแน่นอนเสมอ ความพยายามในการแปลงจำนวนเต็มไปลอยอาจทำให้ความแม่นยำลดลงและทำการเปรียบเทียบที่ไม่ถูกต้อง ความพยายามในการร่ายลอยให้เป็นจำนวนเต็มจะไม่ได้ผลเช่นกันเพราะส่วนที่เป็นเศษส่วนจะหายไป

เพื่อแก้ไขปัญหานี้ Python ดำเนินการตรวจสอบหลายชุดโดยส่งคืนผลลัพธ์หากการตรวจสอบหนึ่งประสบความสำเร็จ มันเปรียบเทียบสัญญาณของทั้งสองค่าจากนั้นไม่ว่าจะเป็นจำนวนเต็ม "ใหญ่เกินไป" ที่จะลอยแล้วเปรียบเทียบตัวแทนของลอยไปที่ความยาวของจำนวนเต็ม หากทุกอย่างของการตรวจสอบเหล่านี้ล้มเหลวก็เป็นสิ่งจำเป็นในการสร้างวัตถุสองหลามใหม่ในการเปรียบเทียบเพื่อให้ได้ผล

เมื่อเปรียบเทียบการลอยvกับจำนวนเต็ม / ยาวwกรณีที่แย่ที่สุดคือ:

  • vและwมีเครื่องหมายเดียวกัน (ทั้งบวกหรือลบทั้งสอง)
  • จำนวนเต็มwมีบิตเพียงพอเล็กน้อยที่สามารถเก็บไว้ในsize_tประเภท (โดยทั่วไปคือ 32 หรือ 64 บิต)
  • จำนวนเต็มwมีอย่างน้อย 49 บิต
  • ตัวแทนของลอยเป็นเช่นเดียวกับจำนวนบิตในvw

และนี่คือสิ่งที่เรามีสำหรับค่าในคำถาม:

>>> 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ฟังก์ชั่นจัดการการเปรียบเทียบระหว่างสองค่าและvw

ด้านล่างนี้เป็นคำอธิบายขั้นตอนโดยขั้นตอนของการตรวจสอบที่มีประสิทธิภาพการทำงาน ความคิดเห็นในแหล่งไพ ธ อนนั้นมีประโยชน์จริง ๆ เมื่อพยายามทำความเข้าใจว่าฟังก์ชันทำอะไรดังนั้นฉันจึงทิ้งมันไว้ในที่ที่เกี่ยวข้อง ฉันได้สรุปการตรวจสอบเหล่านี้ในรายการที่ส่วนท้ายของคำตอบ

แนวคิดหลักคือการแมปวัตถุ Python vและwC สองตัวที่เหมาะสมiและjซึ่งสามารถเปรียบเทียบได้ง่ายเพื่อให้ได้ผลลัพธ์ที่ถูกต้อง ทั้ง Python 2 และ Python 3 ใช้แนวคิดเดียวกันในการทำสิ่งนี้ (ก่อนหน้านี้เพิ่งจัดการintและlongแยกประเภท)

สิ่งแรกที่ต้องทำคือการตรวจสอบว่าvเป็นมั่นเหมาะลอยงูหลามและแผนที่ไปยัง C iคู่ ถัดไปดูฟังก์ชั่นที่ว่ายังเป็นลอยและแผนที่ไปยังคู่w C 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 ตอนนี้ฟังก์ชั่นตรวจสอบว่ามันเป็นจำนวนเต็มหลาม หากเป็นกรณีนี้การทดสอบที่ง่ายที่สุดคือการแยกสัญญาณของ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 คู่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;
    }

การตรวจสอบนี้สองสิ่ง หากเลขชี้กำลังมีค่าน้อยกว่า 0 แสดงว่า float มีค่าน้อยกว่า 1 (และมีขนาดเล็กกว่าจำนวนเต็มใด ๆ ) หรือถ้ายกกำลังน้อยกว่าจำนวนบิตในwแล้วเรามีที่v < |w|มาตั้งแต่ซิก * 2 ยกกำลังน้อยกว่า 2 nbits

wความล้มเหลวในการตรวจสอบทั้งสองลักษณะฟังก์ชั่นเพื่อดูว่าตัวแทนที่มีค่ามากกว่าจำนวนบิตใน นี่แสดงให้เห็นว่าซิกนิแคนด์และ* 2 เลขชี้กำลังมีค่ามากกว่า 2 nbitsดังนั้นv > |w|:

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

หากการตรวจสอบนี้ไม่ประสบความสำเร็จเรารู้ว่าตัวแทนของลอยเป็นเช่นเดียวกับจำนวนบิตในจำนวนเต็มที่vw

วิธีเดียวที่ว่าทั้งสองค่าสามารถนำมาเปรียบเทียบในขณะนี้คือการสร้างจำนวนเต็มสองจำนวนหลามใหม่จากและv wแนวคิดคือการละทิ้งส่วนที่เป็นเศษส่วนของvสองส่วนที่เป็นจำนวนเต็มแล้วเพิ่มอีกหนึ่งส่วน wเพิ่มเป็นสองเท่าและสามารถเปรียบเทียบออบเจ็กต์ Python ใหม่ทั้งสองเพื่อให้ค่าส่งคืนที่ถูกต้อง การใช้ตัวอย่างที่มีค่าน้อย4.65 < 4จะถูกกำหนดโดยการเปรียบเทียบ(2*4)+1 == 9 < 8 == (2*4)(return 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
    }
}

เพื่อความสั้นฉันได้ละทิ้งการตรวจสอบข้อผิดพลาดเพิ่มเติมและการติดตามขยะที่ต้องทำเมื่อสร้างวัตถุใหม่เหล่านี้ ไม่จำเป็นต้องพูดสิ่งนี้เพิ่มค่าใช้จ่ายเพิ่มเติมและอธิบายว่าทำไมค่าที่เน้นสีในคำถามนั้นช้ากว่าการเปรียบเทียบอย่างอื่นมาก


นี่คือบทสรุปของการตรวจสอบที่ดำเนินการโดยฟังก์ชั่นการเปรียบเทียบ

อนุญาตvจะลอยและโยนมันทิ้งเป็น C คู่ ทีนี้ถ้าwยังเป็นแบบลอยตัว:

  • ตรวจสอบว่าwเป็นหรือnan ถ้าเป็นเช่นนั้นจัดการกับกรณีพิเศษนี้แยกกันขึ้นอยู่กับชนิดของinfw

  • ถ้าไม่เปรียบเทียบvและwโดยตรงโดยการเป็นตัวแทนของพวกเขาในฐานะ C คู่ผสม

ถ้าwเป็นจำนวนเต็ม:

  • สารสกัดจากสัญญาณของและv wหากพวกเขาแตกต่างจากนั้นเรารู้vและwแตกต่างและซึ่งเป็นค่าที่มากกว่า

  • ( เครื่องหมายเหมือนกัน ) ตรวจสอบว่าwมีบิตมากเกินไปที่จะลอย (มากกว่าsize_t) ถ้าเป็นเช่นนั้นมีขนาดใหญ่กว่าwv

  • ตรวจสอบว่าwมี 48 หรือน้อยกว่าบิต ถ้าเป็นเช่นนั้นก็สามารถโยนได้อย่างปลอดภัยไปยัง C vคู่โดยไม่สูญเสียความแม่นยำและเมื่อเทียบกับ

  • ( wมีมากกว่า 48 บิต. ตอนนี้เราจะเก็บwเป็นจำนวนเต็มบวกที่มีการเปลี่ยนแปลงสหกรณ์เปรียบเทียบตามความเหมาะสม. )

  • vพิจารณาตัวแทนของลอย หากเลขชี้กำลังเป็นลบvจะมีค่าน้อยกว่า1และเท่ากับจำนวนเต็มบวกใด ๆ มิฉะนั้นถ้าเลขชี้กำลังน้อยกว่าจำนวนบิตในแล้วก็ต้องน้อยกว่าww

  • หากตัวแทนของvมีค่ามากกว่าจำนวนบิตในwนั้นมีค่ามากกว่าvw

  • ( เลขชี้กำลังเป็นเช่นเดียวกับจำนวนบิตในw. )

  • การตรวจสอบขั้นสุดท้าย แบ่งvออกเป็นส่วนจำนวนเต็มและเศษส่วน สองส่วนจำนวนเต็มและเพิ่ม 1 เพื่อชดเชยส่วนที่เป็นเศษส่วน wตอนนี้เป็นสองเท่าของจำนวนเต็ม เปรียบเทียบจำนวนเต็มใหม่สองตัวนี้แทนเพื่อรับผลลัพธ์


4
นักพัฒนา Python ทำได้ดีมาก - การใช้งานภาษาส่วนใหญ่จะมีเพียง handwaved ปัญหาโดยบอกว่าการเปรียบเทียบ float / จำนวนเต็มไม่แน่นอน
user253751

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.