Часи-два швидше, ніж біт-зсув, для цілих чисел Python 3.x?


150

Я дивився на джерело sorted_containers і був здивований, побачивши цей рядок :

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

Ось loadціле число. Навіщо використовувати зсув бітів в одному місці, а множення в іншому? Здається розумним, що зміщення бітів може бути швидшим, ніж інтегральне ділення на 2, але чому б не замінити і множення на зсув? Я визначив такі випадки:

  1. (рази, розділити)
  2. (зміна, зміна)
  3. (разів, зміна)
  4. (зміна, поділ)

і виявив, що №3 стабільно швидше, ніж інші альтернативи:

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

введіть тут опис зображення введіть тут опис зображення

Питання:

Чи правильний мій тест? Якщо так, то чому (множення, зміна) швидше, ніж (зсув, зсув)?

Я запускаю Python 3.5 на Ubuntu 14.04.

Редагувати

Вище - оригінальний виклад питання. Ден Гец дає чудове пояснення у своїй відповіді.

Для повноти наведено приклади ілюстрацій для більшої кількості, xколи оптимізація множення не застосовується.

введіть тут опис зображення введіть тут опис зображення


3
Де ви визначилися x?
JBernardo

3
Я дуже хотів би побачити, чи є якась різниця, використовуючи маленький ендіан / великий ендіан. Дійсно класне питання btw!
LiGhTx117

1
@ LiGhTx117 Я б очікував, що це не має відношення до операцій, якщо xне дуже велике, тому що це лише питання про те, як це зберігається в пам'яті, правда?
Ден Гец

1
Мені цікаво, а як помножити на 0,5, а не ділити на 2? З попереднього досвіду програмування складання миль, поділ, як правило, призводить до операції множення. (Це б пояснило перевагу зсуву бітів замість поділу)
Sayse

2
@Sayse, який перетворив би його на плаваючу точку. Сподіваємось, ціле поділ поверху було б швидшим, ніж кругова поїздка через плаваючу точку.
Ден Гетц

Відповіді:


155

Це, мабуть, тому, що множення малих чисел оптимізовано в CPython 3.5 таким чином, що ліві зрушення на невеликі числа не є. Позитивні зсуви ліворуч завжди створюють більший цілий об'єкт для збереження результату як частини обчислення, тоді як для множень типу, який ви використовували у своєму тесті, спеціальна оптимізація уникає цього і створює цілий об'єкт правильного розміру. Це можна побачити у вихідному коді цілочисельної реалізації Python .

Оскільки цілі числа в Python є довільною точністю, вони зберігаються у вигляді масивів цілих «цифр», з обмеженням на кількість бітів на цілочисельну цифру. Тож у загальному випадку операції, що включають цілі числа, не є одиничними операціями, а натомість потрібно обробляти випадок декількох "цифр". У pyport.h цей бітовий ліміт визначається як 30 біт на 64-бітній платформі, або 15 біт в іншому випадку. (Я просто зателефоную цьому 30 звідси, щоб пояснення було простим. Але зауважте, що якщо ви використовували Python, скомпільований для 32-розрядних, результат вашого орієнтиру залежав би від того, чи xбуло б менше 32,768 чи ні.)

Коли входи та виходи операції залишаються в межах 30-бітової межі, операцію можна обробляти оптимізованим способом замість загального. Початок реалізації цілого множення полягає в наступному:

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
    }

Отже, при множенні двох цілих чисел, де кожне вписується в 30-бітну цифру, це робиться як пряме множення інтерпретатором CPython, а не робота з цілими числами як масиви. (MEDIUM_VALUE() виклик позитивного цілого об'єкта просто отримує свою першу 30-розрядну цифру.) Якщо результат вписується в одну 30-бітну цифру, PyLong_FromLongLong()це помітить у відносно невеликій кількості операцій і створить одноцифровий цілий цілий об'єкт для зберігання це.

На відміну від цього, ліві зрушення не оптимізовані таким чином, і кожен зсув лівого зсуву має справу з цілим числом, яке зміщується як масив. Зокрема, якщо ви подивитесь на вихідний код для long_lshift(), у випадку невеликого, але позитивного лівого зсуву, 2-значний цілочисельний об'єкт завжди створюється, якщо тільки його довжина буде обрізана до 1 пізніше: (мої коментарі у /*** ***/)

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

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

Ціле ділення

Ви не запитували про гірші показники цілого поділу поверху порівняно з правильними зрушеннями, оскільки це відповідало вашим (і моїм) очікуванням. Але ділення невеликого додатного числа на інше невелике додатне число також не настільки оптимізоване, як мале множення. Кожен //обчислює як коефіцієнт, так і залишок, використовуючи функцію long_divrem(). Цей залишок обчислюється для малого дільника з множенням і зберігається в щойно виділеному цілому об'єкті , який у цій ситуації негайно відкидається.


1
Це цікаве спостереження з підрозділом, дякую, що вказали на це. Само собою зрозуміло, що це відмінна відповідь загалом.
hilberts_drinking_problem

Добре досліджена і письмова відповідь на відмінну запитання. Можливо, буде цікаво показати графіки, що визначають терміни виходу за xмежі оптимізованого діапазону.
Бармар
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.