¿Dos veces más rápido que bit shift?


Estaba mirando la fuente de sorted_containers y me sorprendió ver esta línea :

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

Aquí load es un entero. ¿Por qué usar bit shift en un lugar y multiplicación en otro? Parece razonable que el desplazamiento de bits puede ser más rápido que la división integral por 2, pero ¿por qué no reemplazar la multiplicación por un desplazamiento también? Hice referencia a los siguientes casos:

  1. (tiempos, dividir)
  2. (shift, shift)
  3. (veces, shift)
  4. (shift, divide)

Y encontró que #3 es consistentemente más rápido que otras alternativas:

# 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_swaped():
    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_swaped),
    }


def get_observations():
    return pd.DataFrame([observe(k) for k in range(100)])

introduzca la descripción de la imagen aquí introduzca la descripción de la imagen aquí

La pregunta:

¿Es válida mi prueba? Si es así, ¿por qué es (multiplicar, shift) más rápido que (shift, shift)?

Corro Python 3.5 en Ubuntu 14.04.

Editar

Arriba está la declaración original de la pregunta. Dan Getz proporciona una excelente explicación en su respuesta.

Para el en aras de la exhaustividad, aquí hay ejemplos de ilustraciones para x más grandes cuando no se aplican optimizaciones de multiplicación.

introduzca la descripción de la imagen aquí introduzca la descripción de la imagen aquí

Author: Yakym Pirozhenko, 2016-05-05

1 answers

Esto parece ser porque la multiplicación de números pequeños está optimizada en CPython 3.5, de una manera que los desplazamientos a la izquierda por números pequeños no lo están. Los cambios positivos a la izquierda siempre crean un objeto entero más grande para almacenar el resultado, como parte del cálculo, mientras que para las multiplicaciones del tipo que utilizó en su prueba, una optimización especial evita esto y crea un objeto entero del tamaño correcto. Esto se puede ver en el código fuente del entero de Python aplicación .

Debido a que los enteros en Python son de precisión arbitraria, se almacenan como matrices de "dígitos" enteros, con un límite en el número de bits por dígito entero. Así que en el caso general, las operaciones que involucran enteros no son operaciones simples, sino que necesitan manejar el caso de múltiples "dígitos". En pyport.h , este límite de bits se define como 30 bits en una plataforma de 64 bits, o 15 bits de lo contrario. (Voy a llamar a este 30 de aquí en adelante para mantener la explicación simple. Pero tenga en cuenta que si estuviera utilizando Python compilado para 32 bits, el resultado de su benchmark dependería de si x fuera menor que 32,768 o no.)

Cuando las entradas y salidas de una operación permanecen dentro de este límite de 30 bits, la operación se puede manejar de una manera optimizada en lugar de la forma general. El comienzo de la implementación de la multiplicación de enteros es el siguiente:

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
    }

Así que al multiplicar dos enteros donde cada uno encaja en un dígito de 30 bits, esto se realiza como una multiplicación directa por el intérprete CPython, en lugar de trabajar con los enteros como matrices. (MEDIUM_VALUE() invocado en un objeto entero positivo simplemente obtiene su primer dígito de 30 bits.) Si el resultado encaja en un solo dígito de 30 bits, PyLong_FromLongLong() notará esto en un número relativamente pequeño de operaciones, y creará un objeto entero de un solo dígito para almacenarlo.

Por el contrario, los cambios a la izquierda no se optimizan de esta manera, y cada cambio a la izquierda se ocupa del entero desplazado como una matriz. En particular, si nos fijamos en el código fuente para long_lshift(), en el caso de un pequeño pero positivo desplazamiento a la izquierda, un objeto entero de 2 dígitos siempre se crea, aunque solo sea para tener su longitud truncada a 1 más tarde: (mis comentarios en /*** ***/)

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ón entera

No preguntaste por el peor rendimiento de la división de piso entero en comparación con los cambios correctos, porque eso se ajusta a tus (y a mis) expectativas. Pero dividiendo un pequeño número positivo por otro pequeño número positivo no está tan optimizado como pequeñas multiplicaciones, tampoco. Cada // calcula el cociente y el resto usando la función long_divrem(). Este resto se calcula para un divisor pequeño con una multiplicación, y se almacena en un objeto entero recién asignado, que en esta situación se descarta inmediatamente.

 140
Author: Dan Getz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-05-06 19:49:22