Neden bazı kayan <tamsayı karşılaştırmaları diğerlerinden dört kat daha yavaştır?


284

Kayan sayıları tamsayılarla karşılaştırırken, bazı değer çiftlerinin değerlendirilmesi, benzer büyüklükteki diğer değerlerden çok daha uzun sürer.

Örneğin:

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

Ancak, kayan nokta veya tam sayı belirli bir miktar daha küçük veya daha büyük yapılırsa, karşılaştırma çok daha hızlı gerçekleşir:

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

Karşılaştırma operatörünün değiştirilmesi (örneğin ==veya >bunun yerine) süreleri fark edilir şekilde etkilemez.

Bu sadece büyüklükle ilgili değildir, çünkü daha büyük veya daha küçük değerler seçmek daha hızlı karşılaştırmalara neden olabilir, bu yüzden bitlerin sıraya girmesinin talihsiz bir şekilde olduğundan şüpheleniyorum.

Açıkçası, bu değerlerin karşılaştırılması çoğu kullanım durumu için yeterince hızlıdır. Sadece Python'un neden bazı değer çiftleriyle diğerlerinden daha fazla mücadele ettiğini merak ediyorum.


Hem 2.7 hem de 3.x'de aynı mı?
thefourtheye

Yukarıdaki zamanlamalar Python 3.4'ten alınmıştır - 2.7 çalıştıran Linux bilgisayarımda zamanlamalarda benzer bir tutarsızlık vardı (3 ila 4 ve biraz daha yavaş zamanlar).
Alex Riley

1
İlginç yazı için teşekkürler. Soruna neyin ilham verdiğini merak ediyorum - karşılaştırmaları rastgele zamanladınız mı veya arkasında bir hikaye var mı?
Veedrac

3
@Veedrac: Teşekkür ederim. Çok fazla hikaye yok: Yüzen ve tam sayıların ne kadar çabuk karşılaştırıldığını, birkaç değeri zamanladığını ve ufacık farklılıkların farkına vardım. Sonra Python'un şamandıraları ve büyük tam sayıları nasıl doğru bir şekilde karşılaştırmayı başardığına dair hiçbir fikrim olmadığını fark ettim. Kaynağı anlamak için biraz zaman harcadım ve en kötü durumun ne olduğunu öğrendim.
Alex Riley

2
@Yvesdaoust: bu belirli değerler değil, hayır (bu inanılmaz bir şans olurdu!). Çeşitli değer çiftlerini denedim ve zamanlamalarda daha küçük farklılıklar fark ettim (örneğin, küçük büyüklükteki bir şamandırayı benzer tamsayılarla çok büyük tamsayılarla karşılaştırmak). 2 ^ 49 davasını ancak karşılaştırmanın nasıl çalıştığını anlamak için kaynağa baktıktan sonra öğrendim. Sorudaki değerleri seçtim çünkü konuyu en ilgi çekici şekilde sundular.
Alex Riley

Yanıtlar:


354

Kayan nesneler için Python kaynak kodundaki bir yorum şunları kabul eder:

Karşılaştırma neredeyse bir kabus

Bu, bir şamandırayı bir tam sayı ile karşılaştırırken özellikle doğrudur, çünkü şamandıralardan farklı olarak, Python'daki tam sayılar keyfi olarak büyük olabilir ve her zaman kesindir. Tamsayıyı bir kayan noktaya yayınlamaya çalışmak hassasiyeti kaybedebilir ve karşılaştırmayı yanlış yapabilir. Şamandırayı bir tamsayıya atmaya çalışmak da işe yaramayacaktır çünkü herhangi bir kesirli kısım kaybolacaktır.

Bu sorunu aşmak için, Python bir dizi kontrol gerçekleştirir ve kontrollerden biri başarılı olursa sonucu döndürür. İki değerin işaretlerini karşılaştırır, ardından tamsayının bir kayan sayı için "çok büyük" olup olmadığını, ardından kayan öğenin üssünü tam sayı uzunluğuyla karşılaştırır. Tüm bu kontroller başarısız olursa, sonucu elde etmek için iki yeni Python nesnesi oluşturmak gerekir.

Bir şamandırayı vbir tamsayı / uzun ile karşılaştırırken w, en kötü durum şudur:

  • vve waynı işarete sahip (hem pozitif hem de negatif),
  • tamsayı w, size_ttipte tutulabilecek kadar az bite sahiptir (tipik olarak 32 veya 64 bit),
  • tamsayıda wen az 49 bit vardır,
  • şamandıranın üssü, içindeki vbit sayısı ile aynıdır w.

Ve bu, sorudaki değerler için tam olarak sahip olduğumuz şeydir:

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

49'un hem şamandıranın üssü hem de tamsayıdaki bit sayısı olduğunu görüyoruz. Her iki sayı da pozitiftir ve bu nedenle yukarıdaki dört kriter karşılanmıştır.

Değerlerden birini daha büyük (veya daha küçük) olarak seçmek, tamsayının bit sayısını veya üs değerini değiştirebilir ve böylece Python, pahalı son kontrolü gerçekleştirmeden karşılaştırmanın sonucunu belirleyebilir.

Bu, dilin CPython uygulamasına özgüdür.


Daha ayrıntılı karşılaştırma

float_richcompareFonksiyon iki değer arasında karşılaştırma kolları vve w.

Aşağıda, işlevin gerçekleştirdiği denetimlerin adım adım açıklaması verilmiştir. Python kaynağındaki yorumlar, işlevin ne yaptığını anlamaya çalışırken aslında çok yararlıdır, bu yüzden onları ilgili yerlerde bıraktım. Bu kontrolleri cevabın altındaki bir listede de özetledim.

Ana fikir, Python nesnelerini vve wiki uygun C çiftine eşlemektir ive jbu daha sonra doğru sonucu vermek için kolayca karşılaştırılabilir. Hem Python 2 hem de Python 3 bunu yapmak için aynı fikirleri kullanır (eski sadece ayrı ayrı ele alır intve longtürler).

Yapılacak ilk şey v, kesinlikle bir Python şamandırası olup olmadığını kontrol etmek ve bir C çiftine eşlemektir i. Daha sonra fonksiyon waynı zamanda bir şamandıra olup olmadığına bakar ve onu bir C çiftiyle eşler j. Diğer tüm kontroller atlanabileceğinden, bu işlev için en iyi senaryodur. Fonksiyon aynı zamanda kontrol eder olmadığını görmek için vise infya 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;
    }

Şimdi biliyoruz ki wbu kontroller başarısız olursa , bir Python şamandırası değildir. Şimdi fonksiyon bir Python tamsayısı olup olmadığını kontrol ediyor. Bu durumda, en kolay test işaretini vve işaretini ayıklamaktır w( eğer pozitif 0ise sıfır -1ise geri dön 1). İşaretler farklıysa, karşılaştırmanın sonucunu döndürmek için gereken tüm bilgiler budur:

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

Bu onay başarısız olursa, o zaman vve waynı işarete sahip.

Bir sonraki kontrol, tamsayıdaki bit sayısını sayar w. Çok fazla biti varsa, muhtemelen bir şamandıra olarak tutulamaz ve bu nedenle şamandıradan daha büyük olmalıdır 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;
    }

Öte yandan, tamsayı w48 veya daha az bite sahipse, güvenli bir şekilde C çiftine dönüşebilir jve karşılaştırılabilir:

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

Bu noktadan itibaren, bunun w49 veya daha fazla biti olduğunu biliyoruz . wPozitif bir tam sayı olarak davranmak uygun olacaktır , bu nedenle işareti ve karşılaştırma operatörünü gerektiği gibi değiştirin:

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

Şimdi fonksiyon şamandıranın üssüne bakar. Bir şamandıranın anlamlı * 2 üssü olarak yazılabileceğini (işareti yoksayarak) ve anlamlılığın 0,5 ile 1 arasında bir sayıyı temsil ettiğini hatırlayın :

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

Bu iki şeyi kontrol eder. Üs 0'dan küçükse, şamandıra 1'den küçüktür (ve herhangi bir tamsayıdan büyüklükte daha küçüktür). Üs az bit sayısına göre ise Veya, wo zaman buna sahip v < |w|significand * 2 beri üs 2 den aşağı nbits .

Bu iki kontrol başarısız olursa, fonksiyon üssün bit sayısından daha büyük olup olmadığına bakar w. Bu anlamlı * 2 üssünün 2 nbit'ten büyük olduğunu gösterir ve bu nedenle v > |w|:

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

Bu kontrol başarılı olmazsa, şamandıranın üssünün vtamsayıdaki bit sayısı ile aynı olduğunu biliyoruz w.

İki değerleri artık karşılaştırılabilir tek yolu iki yeni Python tamsayılar oluşturmaktır vve w. Fikir, kesirli kısmını atmak, vtamsayı kısmını ikiye katlamak ve sonra bir tane eklemek. wiki katına çıkar ve bu iki yeni Python nesnesi doğru dönüş değerini vermek için karşılaştırılabilir. Küçük değerlere sahip bir örnek kullanılması 4.65 < 4karşılaştırma ile belirlenir (2*4)+1 == 9 < 8 == (2*4)(yanlış döndürme).

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

Kısacası, Python'un bu yeni nesneleri oluştururken yapması gereken ek hata kontrolü ve çöp izleme özelliğini bıraktım. Söylemeye gerek yok, bu ek yük ekler ve soruda vurgulanan değerlerin neden diğerlerine göre önemli ölçüde daha yavaş olduğunu açıklar.


Karşılaştırma fonksiyonu tarafından gerçekleştirilen kontrollerin bir özeti.

Izin vvermek bir şamandıra ve C çift olarak döküm. Şimdi, eğer wbir şamandıra ise:

  • Olmadığını kontrol edin wolduğunu nanveya inf. Öyleyse, bu özel durumu türüne bağlı olarak ayrı ayrı ele alın w.

  • Değilse, karşılaştırmak vve wC çiftlerde olarak temsiller doğrudan.

Eğer wbir tam sayı ise:

  • Belirtileri Özü vve w. Eğer farklılarsa, o zaman biliyoruz vve wfarklıyız ve bu daha büyük bir değerdir.

  • ( İşaretler aynıdır. ) wKayan noktalı olmak için çok fazla bit olup olmadığını kontrol edin (daha fazla size_t). Eğer öyleyse, wdaha büyüktür v.

  • w48 veya daha az bit olup olmadığını kontrol edin . Eğer öyleyse, hassasiyetini kaybetmeden güvenli bir şekilde C çiftine dökülebilir ve ile karşılaştırılabilir v.

  • ( w48'den fazla biti var. Şimdi wkarşılaştırma op'unu uygun şekilde değiştiren pozitif bir tamsayı olarak ele alacağız . )

  • Şamandıranın üssünü düşünün v. Üs negatifse, o zaman herhangi bir pozitif tamsayıdan vküçüktür 1ve bu nedenle küçüktür. Aksi takdirde, üs bit içindeki sayıdan azsa, wo zaman bu sayıdan az olmalıdır w.

  • Üssü vbit sayısından büyükse wo zaman vbüyüktür w.

  • ( Üs, içindeki bit sayısı ile aynıdır w. )

  • Son kontrol. Bölünmüş vonun tamsayı ve kesirli bölüme. Tamsayı kısmını ikiye katlayın ve kesirli kısmı telafi etmek için 1 ekleyin. Şimdi tamsayıyı iki katına çıkarın w. Sonuç almak için bu iki yeni tamsayıyı karşılaştırın.


4
Aferin Python geliştiricileri - çoğu dil uygulaması, kayan nokta / tamsayı karşılaştırmalarının tam olmadığını söyleyerek sorunu elden geçirirdi.
user253751

4

gmpy2Rasgele hassas şamandıralar ve tamsayılarla birlikte kullanmak daha düzgün karşılaştırma performansı elde etmek mümkündür:

~ $ 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
Bu kütüphaneyi henüz kullanmadım, ancak potansiyel olarak çok kullanışlı görünüyor. Teşekkürler!
Alex Riley

Sympy ve mpmath tarafından kullanılır
denfromufa

CPython da decimalstandart kütüphanede var
denfromufa
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.