Время-два быстрее, чем бит-сдвиг, для целых чисел Python 3.x?

Я смотрел на источник 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)])

enter image description here enter image description here

Вопрос:

Является ли мой тест действительным? Если да, то почему (умножить, сдвинуть) быстрее, чем (сдвиг, сдвиг)?

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

редактировать

Выше оригинальная постановка вопроса. Дэн Гетц дает отличное объяснение в своем ответе.

Для полноты здесь приведены примеры иллюстраций для больших x когда оптимизация умножения не применяется.

enter image description here enter image description here

Ответ 1

Это, по-видимому, связано с тем, что умножение небольших чисел оптимизировано в 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(). Этот остаток вычисляется для небольшого делителя с умножением, а хранится в вновь выделенном цельном объекте, который в этой ситуации немедленно отбрасывается.