Python 3.x tamsayıları için iki kat daha hızlı bit kaydırma?


150

Sort_containers kaynağına bakıyordum ve bu satırı görünce şaşırdım :

self._load, self._twice, self._half = load, load * 2, load >> 1

İşte loadbir tamsayı. Neden bir yerde bit kaydırma, başka bir yerde çarpma kullanılır? Bit kaydırmanın 2'ye bölünme işleminden daha hızlı olabileceği makul görünüyor, ancak neden çarpmayı bir kaydırma ile değiştirmiyorsunuz? Aşağıdaki durumları karşılaştırdım:

  1. (kez, böl)
  2. (üst karakter, üst karakter)
  3. (kez, vardiya)
  4. (kaydırma, bölme)

ve # 3'ün diğer alternatiflerden daha hızlı olduğunu tespit etti:

# self._load, self._twice, self._half = load, load * 2, load >> 1

import random
import timeit
import pandas as pd

x = random.randint(10 ** 3, 10 ** 6)

def test_naive():
    a, b, c = x, 2 * x, x // 2

def test_shift():
    a, b, c = x, x << 1, x >> 1    

def test_mixed():
    a, b, c = x, x * 2, x >> 1    

def test_mixed_swapped():
    a, b, c = x, x << 1, x // 2

def observe(k):
    print(k)
    return {
        'naive': timeit.timeit(test_naive),
        'shift': timeit.timeit(test_shift),
        'mixed': timeit.timeit(test_mixed),
        'mixed_swapped': timeit.timeit(test_mixed_swapped),
    }

def get_observations():
    return pd.DataFrame([observe(k) for k in range(100)])

resim açıklamasını buraya girin resim açıklamasını buraya girin

Soru:

Testim geçerli mi? Öyleyse, neden (çarpma, kaydırma) neden (kaydırma, kaydırma) daha hızlıdır?

Ubuntu 14.04'te Python 3.5 çalıştırıyorum.

Düzenle

Yukarıdaki sorunun orijinal ifadesidir. Dan Getz cevabında mükemmel bir açıklama yapıyor.

Tamlık uğruna, xçarpma optimizasyonları uygulanmadığında daha büyük örnek resimler .

resim açıklamasını buraya girin resim açıklamasını buraya girin


3
Nerede tanımladın x?
JBernardo

3
Gerçekten küçük endian / büyük endian kullanarak herhangi bir fark olup olmadığını görmek istiyorum. Gerçekten harika bir soru btw!
LiGhTx117

1
@ LiGhTx117 xÇok büyük olmadıkça , işlemlerle ilgisiz olmasını beklerdim , çünkü bu sadece hafızada nasıl saklandığı sorusu, değil mi?
Dan Getz

1
Merak ediyorum, 2'ye bölmek yerine 0,5 ile çarpmaya ne dersiniz? Mips montaj programlama ile ilgili önceki deneyimlerden, bölüm normalde yine de bir çarpma işlemi ile sonuçlanır. (Bu, bölünme yerine bit kaydırma tercihini açıklar)
Sayse

2
@Sayma, onu kayan noktaya dönüştürür. Umarım tamsayı kat bölümü kayan noktadan bir gidiş-dönüşten daha hızlı olur.
Dan Getz

Yanıtlar:


155

Bunun nedeni, CPython 3.5'te küçük sayıların çoğalmasının, küçük sayılara göre sol kaymaların olmayacağı şekilde optimize edilmesidir. Pozitif sola kaydırmalar, hesaplamanın bir parçası olarak sonucu saklamak için her zaman daha büyük bir tamsayı nesnesi yaratırken, testinizde kullandığınız sıralamanın çarpımı için özel bir optimizasyon bunu önler ve doğru boyutta bir tamsayı nesnesi oluşturur. Bu, Python'un tamsayı uygulamasının kaynak kodunda görülebilir .

Python'daki tamsayılar hassas olduğundan, tamsayı basamağı başına bit sayısında bir sınır ile tamsayı "basamak" dizileri olarak saklanır. Bu nedenle, genel durumda, tamsayıları içeren işlemler tek işlem değildir, bunun yerine birden çok "basamak" durumunda işlem yapması gerekir. Olarak pyport.h bu bit sınır olarak tanımlanmaktadır aksi 64 bit platformunda 30 bit veya 15 bit. (Açıklamayı basit tutmak için buradan 30'u arayacağım. Ancak 32 bit için derlenmiş Python kullanıyorsanız, karşılaştırmanızın sonucunun x32.768'den az olup olmamasına bağlı olacağını unutmayın.)

Bir işlemin giriş ve çıkışları bu 30 bitlik sınırın içinde kaldığında, işlem genel yol yerine optimize edilmiş bir şekilde işlenebilir. Tamsayı çarpım uygulamasının başlangıcı aşağıdaki gibidir:

static PyObject *
long_mul(PyLongObject *a, PyLongObject *b)
{
    PyLongObject *z;

    CHECK_BINOP(a, b);

    /* fast path for single-digit multiplication */
    if (Py_ABS(Py_SIZE(a)) <= 1 && Py_ABS(Py_SIZE(b)) <= 1) {
        stwodigits v = (stwodigits)(MEDIUM_VALUE(a)) * MEDIUM_VALUE(b);
#ifdef HAVE_LONG_LONG
        return PyLong_FromLongLong((PY_LONG_LONG)v);
#else
        /* if we don't have long long then we're almost certainly
           using 15-bit digits, so v will fit in a long.  In the
           unlikely event that we're using 30-bit digits on a platform
           without long long, a large v will just cause us to fall
           through to the general multiplication code below. */
        if (v >= LONG_MIN && v <= LONG_MAX)
            return PyLong_FromLong((long)v);
#endif
    }

Dolayısıyla, her birinin 30 bitlik bir basamağa sığdığı iki tamsayıyı çarparken, bu, tamsayılarla diziler olarak çalışmak yerine, CPython yorumlayıcısı tarafından doğrudan çarpma olarak yapılır. ( MEDIUM_VALUE()pozitif bir tamsayı nesnesinde çağrıldığında ilk 30 bit basamağını alır.) Sonuç tek bir 30 bit basamağa sığarsa, PyLong_FromLongLong()bunu nispeten az sayıda işlemde fark eder ve saklamak için tek basamaklı bir tam sayı nesnesi oluşturur o.

Buna karşılık, sola kaydırma bu şekilde optimize edilmez ve her sola kaydırma, tamsayı dizi olarak kaydırılmakla ilgilenir. Özellikle, kaynak koduna bakarsanız, long_lshift()küçük ama pozitif bir sola kaydırma durumunda, yalnızca uzunluğunun daha sonra 1'e kesilmesi durumunda her zaman 2 basamaklı bir tam sayı nesnesi oluşturulur: (yorumlarımda /*** ***/)

static PyObject *
long_lshift(PyObject *v, PyObject *w)
{
    /*** ... ***/

    wordshift = shiftby / PyLong_SHIFT;   /*** zero for small w ***/
    remshift  = shiftby - wordshift * PyLong_SHIFT;   /*** w for small w ***/

    oldsize = Py_ABS(Py_SIZE(a));   /*** 1 for small v > 0 ***/
    newsize = oldsize + wordshift;
    if (remshift)
        ++newsize;   /*** here newsize becomes at least 2 for w > 0, v > 0 ***/
    z = _PyLong_New(newsize);

    /*** ... ***/
}

Tam sayı bölümü

Tam vardiya bölümünün doğru vardiyalara kıyasla daha kötü performansını sormadınız, çünkü bu sizin (ve benim) beklentilerinize uyuyor. Ancak küçük bir pozitif sayıyı başka bir küçük pozitif sayıya bölmek de küçük çarpmalar kadar optimize edilmez. Her biri fonksiyonu kullanarak //hem bölümü hem de geri kalanını hesaplar long_divrem(). Bu kalan, çarpma ile küçük bir bölen için hesaplanır ve bu durumda hemen atılan yeni tahsis edilmiş bir tam sayı nesnesinde saklanır .


1
Bölme ile ilginç bir gözlem, işaret ettiğiniz için teşekkürler. Bunun genel olarak mükemmel bir cevap olduğunu söylemeye gerek yok.
hilberts_drinking_problem

Mükemmel bir soruya iyi araştırılmış ve yazılı bir cevap. xOptimize edilmiş aralığın dışında kalan zamanlama için grafikler göstermek ilginç olabilir .
Barmar
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.