Eu estava olhando para a fonte de sortidos_containers e fiquei surpreso ao ver esta linha :
self._load, self._twice, self._half = load, load * 2, load >> 1
Aqui load
está um número inteiro. Por que usar deslocamento de bits em um lugar e multiplicação em outro? Parece razoável que a troca de bits seja mais rápida que a divisão integral por 2, mas por que não substituir a multiplicação por uma troca também? Comparei os seguintes casos:
- (vezes, dividir)
- (turno, turno)
- (horários, turno)
- (mudar, dividir)
e descobrimos que o nº 3 é consistentemente mais rápido do que outras 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_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)])
A questão:
Meu teste é válido? Se sim, por que (multiplicar, mudar) é mais rápido que (mudar, mudar)?
Eu executo o Python 3.5 no Ubuntu 14.04.
Editar
Acima está a declaração original da pergunta. Dan Getz fornece uma excelente explicação em sua resposta.
Por uma questão de integridade, aqui estão exemplos de ilustrações para maiores x
quando as otimizações de multiplicação não se aplicam.
fonte
x
?x
seja muito grande, porque isso é apenas uma questão de como é armazenado na memória, certo?Respostas:
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:
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/*** ***/
)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çãolong_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.fonte
x
fora do intervalo otimizado.