Due volte più veloce del bit-shift, per gli interi Python 3.x?


150

Stavo guardando la fonte di sort_containers e sono stato sorpreso di vedere questa linea :

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

Ecco loadun numero intero. Perché usare il bit shift in un posto e la moltiplicazione in un altro? Sembra ragionevole che lo spostamento dei bit possa essere più veloce della divisione integrale per 2, ma perché non sostituire anche la moltiplicazione con uno spostamento? Ho confrontato i seguenti casi:

  1. (volte, dividi)
  2. (spostamento, spostamento)
  3. (tempi, turni)
  4. (sposta, dividi)

e ho scoperto che il n. 3 è costantemente più veloce di altre alternative:

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

inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine

La domanda:

Il mio test è valido? In tal caso, perché (moltiplicare, spostare) è più veloce di (spostare, spostare)?

Corro Python 3.5 su Ubuntu 14.04.

modificare

Sopra è la dichiarazione originale della domanda. Dan Getz fornisce un'eccellente spiegazione nella sua risposta.

Per completezza, ecco alcune illustrazioni di esempio per dimensioni maggiori xquando non si applicano le ottimizzazioni della moltiplicazione.

inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine


3
Dove hai definito x?
JBernardo,

3
Mi piacerebbe davvero vedere se ci sono differenze usando little endian / big endian. Domanda davvero interessante tra noi!
LiGhTx117,

1
@ LiGhTx117 Mi aspetto che non sia correlato alle operazioni, a meno che non xsia molto grande, perché è solo una questione di come viene archiviato in memoria, giusto?
Dan Getz,

1
Sono curioso, che ne dici di moltiplicare per 0,5 invece di dividere per 2? Dalla precedente esperienza con la programmazione dell'assemblaggio di mips, la divisione normalmente comporta comunque un'operazione di moltiplicazione. (Ciò spiegherebbe la preferenza dello spostamento dei bit invece della divisione)
Sayse,

2
@Sayse che lo convertirà in virgola mobile. Speriamo che la divisione del piano intero sia più veloce di un giro circolare in virgola mobile.
Dan Getz,

Risposte:


155

Ciò sembra essere dovuto al fatto che la moltiplicazione di piccoli numeri è ottimizzata in CPython 3.5, in modo tale che non lo siano gli spostamenti a sinistra per piccoli numeri. I turni a sinistra positivi creano sempre un oggetto intero più grande per memorizzare il risultato, come parte del calcolo, mentre per le moltiplicazioni dell'ordinamento utilizzato nel test, una speciale ottimizzazione evita questo e crea un oggetto intero della dimensione corretta. Questo può essere visto nel codice sorgente dell'implementazione intera di Python .

Poiché gli interi in Python sono di precisione arbitraria, vengono memorizzati come matrici di "cifre" intere, con un limite al numero di bit per cifra intera. Quindi, nel caso generale, le operazioni che coinvolgono numeri interi non sono singole operazioni, ma devono invece gestire il caso di più "cifre". In pyport.h , questo limite di bit è definito come 30 bit su piattaforma a 64 bit o altrimenti 15 bit. (Chiamerò questo 30 da qui in poi per mantenere la spiegazione semplice. Ma nota che se stavi usando Python compilato per 32-bit, il risultato del tuo benchmark dipenderebbe sex fosse inferiore a 32.768 o meno.)

Quando gli ingressi e le uscite di un'operazione rimangono entro questo limite di 30 bit, l'operazione può essere gestita in modo ottimizzato anziché in modo generale. L'inizio del moltiplicazione dei numeri interi è il seguente:

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
    }

Quindi, quando si moltiplicano due numeri interi in cui ciascuno si inserisce in una cifra di 30 bit, questo viene fatto come una moltiplicazione diretta dall'interprete CPython, invece di lavorare con i numeri interi come array. (MEDIUM_VALUE() chiamato su un oggetto intero positivo ottiene semplicemente la sua prima cifra di 30 bit.) Se il risultato si adatta a una singola cifra di 30 bit, PyLong_FromLongLong()lo noterà in un numero relativamente piccolo di operazioni e creerà un oggetto intero a una cifra da memorizzare esso.

Al contrario, gli spostamenti a sinistra non sono ottimizzati in questo modo e ogni spostamento a sinistra si occupa dell'intero spostamento come array. In particolare, se si osserva il codice sorgente per long_lshift(), nel caso di uno spostamento sinistro piccolo ma positivo, viene sempre creato un oggetto intero a 2 cifre, se non altro per avere la sua lunghezza troncata a 1 in seguito: (i miei commenti in /*** ***/)

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

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

Divisione intera

Non hai chiesto le prestazioni peggiori della divisione dei piani interi rispetto ai turni giusti, perché soddisfano le tue (e mie) aspettative. Ma anche la divisione di un piccolo numero positivo per un altro piccolo numero positivo non è ottimizzata come le piccole moltiplicazioni. Ognuno //calcola sia il quoziente che il resto usando la funzione long_divrem(). Questo resto viene calcolato per un piccolo divisore con una moltiplicazione e viene archiviato in un oggetto intero appena allocato , che in questa situazione viene immediatamente scartato.


1
È un'osservazione interessante con la divisione, grazie per averlo sottolineato. Inutile dire che questa è una risposta eccellente nel complesso.
hilberts_drinking_problem

Una risposta ben studiata e scritta a una domanda eccellente. Potrebbe essere interessante mostrare grafici per i tempi al di xfuori dell'intervallo ottimizzato.
Barmar,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.