Lần hai nhanh hơn so với bit-shift, đối với số nguyên Python 3.x?


150

Tôi đã xem nguồn của sort_containers và rất ngạc nhiên khi thấy dòng này :

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

Đây loadlà một số nguyên. Tại sao sử dụng dịch chuyển bit ở một nơi và nhân ở một nơi khác? Có vẻ hợp lý khi dịch chuyển bit có thể nhanh hơn phân chia tích phân bằng 2, nhưng tại sao không thay thế phép nhân bằng một ca làm việc? Tôi đã điểm chuẩn các trường hợp sau:

  1. (lần, chia)
  2. (ca, ca)
  3. (lần, ca)
  4. (thay đổi, phân chia)

và thấy rằng # 3 luôn nhanh hơn các lựa chọn thay thế khác:

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

nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây

Câu hỏi:

Bài kiểm tra của tôi có hợp lệ không? Nếu vậy, tại sao (nhân, thay đổi) nhanh hơn (thay đổi, thay đổi)?

Tôi chạy Python 3.5 trên Ubuntu 14.04.

Biên tập

Trên đây là tuyên bố ban đầu của câu hỏi. Dan Getz cung cấp một lời giải thích tuyệt vời trong câu trả lời của mình.

Để hoàn thiện, đây là các minh họa mẫu cho lớn hơn xkhi tối ưu hóa nhân không được áp dụng.

nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây


3
Bạn đã xác định ở xđâu?
JBernardo

3
Tôi thực sự muốn xem nếu có bất kỳ sự khác biệt bằng cách sử dụng ít endian / endian lớn. Btw câu hỏi thực sự mát mẻ!
LiGhTx117

1
@ LiGhTx117 Tôi hy vọng rằng nó không liên quan đến các hoạt động, trừ khi xrất lớn, bởi vì đó chỉ là một câu hỏi về cách nó được lưu trữ trong bộ nhớ, phải không?
Dan Getz

1
Tôi tò mò, còn nhân 0,5 thì thay vì chia cho 2 thì sao? Từ kinh nghiệm trước đây với lập trình lắp ráp mips, phân chia thông thường sẽ dẫn đến một hoạt động nhân. (Điều đó sẽ giải thích sở thích của dịch chuyển bit thay vì phân chia)
Sayse

2
@Sayse sẽ chuyển đổi nó thành điểm nổi. Hy vọng phân chia sàn số nguyên sẽ nhanh hơn một chuyến đi khứ hồi qua điểm nổi.
Dan Getz

Câu trả lời:


155

Điều này có vẻ là do phép nhân số lượng nhỏ được tối ưu hóa trong CPython 3.5, theo cách mà dịch chuyển trái với số lượng nhỏ thì không. Các dịch chuyển trái tích cực luôn tạo ra một đối tượng số nguyên lớn hơn để lưu trữ kết quả, như là một phần của phép tính, trong khi đối với phép nhân của loại bạn đã sử dụng trong thử nghiệm của mình, một tối ưu hóa đặc biệt sẽ tránh điều này và tạo ra một đối tượng số nguyên có kích thước chính xác. Điều này có thể được nhìn thấy trong mã nguồn của việc triển khai số nguyên của Python .

Vì các số nguyên trong Python là chính xác tùy ý, chúng được lưu trữ dưới dạng các mảng "chữ số" nguyên, với giới hạn về số bit trên mỗi chữ số nguyên. Vì vậy, trong trường hợp chung, các hoạt động liên quan đến số nguyên không phải là các hoạt động đơn lẻ, mà thay vào đó cần xử lý trường hợp có nhiều "chữ số". Trong pyport.h , giới hạn bit này được xác định là 30 bit trên nền tảng 64 bit, hoặc 15 bit khác. (Tôi sẽ chỉ gọi số 30 này từ đây để giữ cho lời giải thích đơn giản. Nhưng lưu ý rằng nếu bạn đang sử dụng Python được biên dịch cho 32 bit, kết quả điểm chuẩn của bạn sẽ phụ thuộc vào việcx có nhỏ hơn 32.768 hay không.)

Khi đầu vào và đầu ra của một hoạt động nằm trong giới hạn 30 bit này, hoạt động có thể được xử lý theo cách tối ưu hóa thay vì cách chung. Bắt đầu thực hiện phép nhân số nguyên như sau:

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
    }

Vì vậy, khi nhân hai số nguyên trong đó mỗi số trùng với một chữ số 30 bit, điều này được thực hiện dưới dạng phép nhân trực tiếp bởi trình thông dịch CPython, thay vì làm việc với các số nguyên dưới dạng mảng. ( MEDIUM_VALUE()được gọi trên một đối tượng số nguyên dương chỉ đơn giản nhận được chữ số 30 bit đầu tiên của nó.) Nếu kết quả khớp với một chữ số 30 bit duy nhất,PyLong_FromLongLong() sẽ nhận thấy điều này trong một số lượng hoạt động tương đối nhỏ và tạo một đối tượng số nguyên một chữ số để lưu trữ nó

Ngược lại, các ca làm việc bên trái không được tối ưu hóa theo cách này và mỗi ca làm việc bên trái liên quan đến số nguyên được dịch chuyển dưới dạng một mảng. Cụ thể, nếu bạn xem mã nguồn long_lshift(), trong trường hợp dịch chuyển trái nhỏ nhưng dương, một đối tượng số nguyên 2 chữ số luôn được tạo, nếu chỉ để rút ngắn độ dài thành 1 sau: (nhận xét của tôi trong /*** ***/)

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

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

Bộ phận nguyên

Bạn đã không hỏi về hiệu suất kém hơn của phân chia số nguyên so với ca phải, bởi vì điều đó phù hợp với kỳ vọng của bạn (và của tôi). Nhưng việc chia một số dương nhỏ cho một số dương nhỏ khác cũng không được tối ưu hóa như các phép nhân nhỏ. Mỗi //tính toán cả thương phần còn lại sử dụng hàm long_divrem(). Phần còn lại này được tính cho một ước số nhỏ với phép nhânđược lưu trữ trong một đối tượng số nguyên mới được phân bổ , trong trường hợp này sẽ bị loại bỏ ngay lập tức.


1
Đó là một quan sát thú vị với bộ phận, cảm ơn vì đã chỉ ra nó. Không cần phải nói rằng đây là một câu trả lời xuất sắc tổng thể.
hilberts_drinking_propet

Một câu trả lời bằng văn bản adn nghiên cứu tốt cho một câu hỏi xuất sắc. Thật thú vị khi hiển thị biểu đồ cho thời gian xbên ngoài phạm vi được tối ưu hóa.
Barmar
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.