Parece que isso ocorre porque a multiplicação de números pequenos é otimizada no CPython 3.5, de uma maneira que as mudanças à esquerda em números pequenos não são. Deslocamentos à esquerda positivos sempre criam um objeto inteiro maior para armazenar o resultado, como parte do cálculo, enquanto que para multiplicações do tipo usado em seu teste, uma otimização especial evita isso e cria um objeto inteiro do tamanho correto. Isso pode ser visto no código fonte da implementação inteira do Python .
Como os números inteiros no Python são de precisão arbitrária, eles são armazenados como matrizes de "dígitos" inteiros, com um limite no número de bits por dígito inteiro. Portanto, no caso geral, operações envolvendo números inteiros não são operações únicas, mas precisam lidar com o caso de vários "dígitos". Em pyport.h , esse limite de bits é definido como 30 bits na plataforma de 64 bits ou 15 bits de outra forma. (Vou chamar esse número de 30 daqui para frente para manter a explicação simples. Mas observe que, se você estivesse usando o Python compilado para 32 bits, o resultado do seu benchmark dependeria se x
fosse inferior a 32.768 ou não.)
Quando as entradas e saídas de uma operação ficam dentro desse limite de 30 bits, a operação pode ser manipulada de maneira otimizada, em vez da maneira geral. O início da implementação de multiplicação de números inteiros é o seguinte:
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
}
Portanto, ao multiplicar dois números inteiros em que cada um se encaixa em um dígito de 30 bits, isso é feito como uma multiplicação direta pelo interpretador CPython, em vez de trabalhar com os números inteiros como matrizes. ( MEDIUM_VALUE()
chamado em um objeto inteiro positivo simplesmente obtém seu primeiro dígito de 30 bits.) Se o resultado couber em um único dígito de 30 bits, PyLong_FromLongLong()
notará isso em um número relativamente pequeno de operações e criará um objeto inteiro de um dígito para armazenar isto.
Por outro lado, os desvios à esquerda não são otimizados dessa maneira, e todo desvio à esquerda lida com o número inteiro sendo alterado como uma matriz. Em particular, se você olhar para o código-fonte long_lshift()
, no caso de um deslocamento à esquerda pequeno mas positivo, um objeto inteiro de 2 dígitos sempre será criado, apenas para ter seu comprimento truncado para 1 posteriormente: (meus comentários em /*** ***/
)
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);
/*** ... ***/
}
Divisão inteira
Você não perguntou sobre o pior desempenho da divisão de piso inteiro em comparação com as mudanças certas, porque isso se encaixa em suas (e minhas) expectativas. Mas dividir um pequeno número positivo por outro pequeno número positivo também não é tão otimizado quanto pequenas multiplicações. Cada //
calcula tanto o quociente e o restante utilizando a função long_divrem()
. Esse restante é calculado para um pequeno divisor com uma multiplicação e é armazenado em um objeto inteiro recém-alocado , que nessa situação é descartado imediatamente.
x
?