В два раза быстрее, чем бит-сдвиг, для целых чисел 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когда оптимизации умножения не применяются.

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

hilberts_drinking_problem
источник
3
Где вы определили x?
Дж
3
Я действительно хотел бы видеть, есть ли какая-либо разница, используя little endian / big endian. Действительно крутой вопрос, кстати!
LiGhTx117
1
@ LiGhTx117 Я ожидаю, что это не будет связано с операциями, если только xоно не очень большое, потому что это просто вопрос того, как оно хранится в памяти, верно?
Дан Гетц
1
Мне любопытно, а как насчет умножения на 0,5 вместо деления на 2? Из предыдущего опыта программирования mips-ассемблирования деление обычно в любом случае приводит к операции умножения. (Это объясняет предпочтение сдвига битов вместо деления)
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(), в случае небольшого, но положительного сдвига влево, всегда создается двузначный целочисленный объект, если только его длина будет усечена до 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пределами оптимизированного диапазона.
Бармар