Kali-dua lebih cepat daripada bit-shift, untuk bilangan bulat Python 3.x?

150

Saya melihat sumber dari Sort_containers dan terkejut melihat baris ini :

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

Berikut loadini adalah bilangan bulat. Mengapa menggunakan bit shift di satu tempat, dan multiplikasi di tempat lain? Tampaknya masuk akal bahwa pergeseran bit mungkin lebih cepat daripada pembagian integral dengan 2, tetapi mengapa tidak mengganti perkalian dengan pergeseran juga? Saya membandingkan beberapa kasus berikut:

  1. (kali, bagi)
  2. (bergeser, bergeser)
  3. (kali, bergeser)
  4. (bergeser, bagilah)

dan menemukan bahwa # 3 secara konsisten lebih cepat daripada alternatif lain:

# 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)])

masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini

Pertanyaan:

Apakah tes saya valid? Jika demikian, mengapa (multiply, shift) lebih cepat dari (shift, shift)?

Saya menjalankan Python 3.5 di Ubuntu 14.04.

Edit

Di atas adalah pernyataan asli dari pertanyaan itu. Dan Getz memberikan penjelasan yang sangat baik dalam jawabannya.

Demi kelengkapan, berikut adalah contoh ilustrasi untuk ukuran yang lebih besar xketika optimisasi multiplikasi tidak berlaku.

masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini

hilberts_drinking_problem
sumber
3
Di mana Anda mendefinisikan x?
JBernardo
3
Saya benar-benar ingin melihat apakah ada perbedaan menggunakan little endian / big endian. Btw pertanyaan yang sangat keren!
LiGhTx117
1
@ LiGhTx117 Saya berharap itu tidak terkait dengan operasi, kecuali xsangat besar, karena itu hanya pertanyaan tentang bagaimana itu disimpan dalam memori, kan?
Dan Getz
1
Saya ingin tahu, bagaimana dengan mengalikan dengan 0,5 bukannya membaginya dengan 2? Dari pengalaman sebelumnya dengan pemrograman perakitan mips, pembagian biasanya menghasilkan operasi perkalian. (Itu akan menjelaskan preferensi sedikit bergeser daripada pembagian)
Sayse
2
@ Katakan itu akan mengubahnya menjadi floating point. Semoga pembagian lantai integer akan lebih cepat daripada perjalanan pulang-pergi melalui floating point.
Dan Getz

Jawaban:

155

Ini tampaknya karena penggandaan angka kecil dioptimalkan dalam CPython 3.5, dengan cara yang tidak bergeser oleh angka kecil tidak. Pergeseran kiri positif selalu membuat objek bilangan bulat yang lebih besar untuk menyimpan hasilnya, sebagai bagian dari perhitungan, sedangkan untuk perkalian dari jenis yang Anda gunakan dalam pengujian Anda, optimasi khusus menghindari hal ini dan membuat objek bilangan bulat dengan ukuran yang benar. Ini dapat dilihat pada kode sumber implementasi integer Python .

Karena integer dengan Python presisi-arbitrary, mereka disimpan sebagai array dari integer "digit", dengan batas jumlah bit per digit integer. Jadi dalam kasus umum, operasi yang melibatkan bilangan bulat bukan operasi tunggal, melainkan harus menangani kasus beberapa "digit". Di pyport.h , batas bit ini ini didefinisikan sebagai 30 bit pada platform 64-bit, atau 15 bit sebaliknya. (Saya hanya akan memanggil 30 ini dari sini untuk menjaga penjelasannya sederhana. Tetapi perhatikan bahwa jika Anda menggunakan Python yang dikompilasi untuk 32-bit, hasil patokan Anda akan tergantung pada apakah xkurang dari 32.768 atau tidak.)

Ketika input dan output operasi tetap dalam batas 30-bit ini, operasi dapat ditangani dengan cara yang dioptimalkan daripada cara umum. Awal implementasi multiplikasi integer adalah sebagai berikut:

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
    }

Jadi ketika mengalikan dua bilangan bulat di mana masing-masing cocok dalam digit 30-bit, ini dilakukan sebagai perkalian langsung oleh juru bahasa CPython, alih-alih bekerja dengan bilangan bulat sebagai array. ( MEDIUM_VALUE()dipanggil pada objek integer positif hanya mendapatkan digit 30-bit pertamanya.) Jika hasilnya cocok dengan digit 30-bit tunggal,PyLong_FromLongLong() akan melihat ini dalam jumlah operasi yang relatif kecil, dan membuat objek integer digit tunggal untuk menyimpan Itu.

Sebaliknya, shift kiri tidak dioptimalkan dengan cara ini, dan setiap shift kiri berkaitan dengan integer yang digeser sebagai array. Secara khusus, jika Anda melihat kode sumber untuk long_lshift(), dalam kasus shift kiri kecil tapi positif, objek integer 2 digit selalu dibuat, jika hanya untuk panjangnya dipotong menjadi 1 nanti: (komentar saya di /*** ***/)

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

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

Divisi integer

Anda tidak bertanya tentang kinerja divisi bilangan bulat yang lebih buruk dibandingkan dengan shift yang tepat, karena itu sesuai dengan harapan Anda (dan saya). Tetapi membagi angka positif kecil dengan angka positif kecil lainnya juga tidak dioptimalkan seperti perkalian kecil. Setiap //menghitung hasil bagi dan sisanya menggunakan fungsi long_divrem(). Sisa ini dihitung untuk pembagi kecil dengan perkalian , dan disimpan dalam objek integer yang baru dialokasikan , yang dalam situasi ini segera dibuang.

Dan Getz
sumber
1
Ini adalah pengamatan yang menarik dengan divisi ini, terima kasih telah menunjukkannya. Tak perlu dikatakan bahwa ini adalah jawaban yang sangat baik secara keseluruhan.
hilberts_drinking_problem
Sebuah jawaban yang diteliti dan ditulis dengan baik untuk pertanyaan yang sangat bagus. Mungkin menarik untuk menampilkan grafik sesuai waktunyax luar rentang yang dioptimalkan.
Barmar