Ao comparar flutuações com números inteiros, alguns pares de valores levam muito mais tempo para serem avaliados do que outros valores de magnitude semelhante.
Por exemplo:
>>> import timeit
>>> timeit.timeit("562949953420000.7 < 562949953421000") # run 1 million times
0.5387085462592742
Mas se o número flutuante ou número inteiro for menor ou maior em uma certa quantidade, a comparação será executada muito mais rapidamente:
>>> timeit.timeit("562949953420000.7 < 562949953422000") # integer increased by 1000
0.1481498428446173
>>> timeit.timeit("562949953423001.8 < 562949953421000") # float increased by 3001.1
0.1459577925548956
Alterar o operador de comparação (por exemplo, usar ==
ou >
não) não afeta os horários de maneira perceptível.
Isso não está relacionado apenas à magnitude, porque a escolha de valores maiores ou menores pode resultar em comparações mais rápidas, então eu suspeito que esteja relacionado a alguma maneira infeliz de alinhar os bits.
Claramente, comparar esses valores é mais do que rápido o suficiente para a maioria dos casos de uso. Estou simplesmente curioso para saber por que o Python parece lutar mais com alguns pares de valores do que com outros.
fonte
Respostas:
Um comentário no código-fonte Python para objetos float reconhece que:
Isso é especialmente verdadeiro quando se compara um float a um número inteiro, porque, diferentemente dos floats, os números inteiros no Python podem ser arbitrariamente grandes e sempre exatos. Tentar converter o número inteiro em um flutuador pode perder precisão e tornar a comparação imprecisa. Tentar converter o float em um número inteiro também não funcionará porque qualquer parte fracionária será perdida.
Para contornar esse problema, o Python executa uma série de verificações, retornando o resultado se uma delas for bem-sucedida. Ele compara os sinais dos dois valores e, em seguida, se o número inteiro é "muito grande" para ser um ponto flutuante, compara o expoente do ponto flutuante com o comprimento do número inteiro. Se todas essas verificações falharem, é necessário construir dois novos objetos Python para comparar, a fim de obter o resultado.
Ao comparar um float
v
com um número inteiro / comprimentow
, o pior caso é o seguinte:v
ew
tem o mesmo sinal (positivo ou negativo),w
tem poucos bits suficientes para ser retido nosize_t
tipo (normalmente 32 ou 64 bits),w
tem pelo menos 49 bits,v
é o mesmo que o número de bitsw
.E é exatamente isso que temos para os valores da pergunta:
Vemos que 49 é o expoente da flutuação e o número de bits no número inteiro. Ambos os números são positivos e, portanto, os quatro critérios acima são atendidos.
A escolha de um dos valores para ser maior (ou menor) pode alterar o número de bits do número inteiro ou o valor do expoente, e assim o Python é capaz de determinar o resultado da comparação sem realizar a verificação final cara.
Isso é específico para a implementação do CPython da linguagem.
A comparação em mais detalhes
A
float_richcompare
função lida com a comparação entre dois valoresv
ew
.Abaixo está uma descrição passo a passo das verificações que a função executa. Os comentários na fonte do Python são realmente muito úteis ao tentar entender o que a função faz, então eu os deixei onde relevantes. Também resumi essas verificações em uma lista no pé da resposta.
A idéia principal é mapear os objetos Python
v
ew
duas duplicatas C apropriadas,i
ej
, que podem ser facilmente comparadas para fornecer o resultado correto. Tanto o Python 2 quanto o Python 3 usam as mesmas idéias para fazer isso (o primeiro apenas manipulaint
elong
digita separadamente).A primeira coisa a fazer é verificar se
v
é definitivamente um flutuador Python e mapeá-lo para um C duploi
. Em seguida, a função examina sew
também é um flutuador e o mapeia para um C duploj
. Este é o melhor cenário para a função, pois todas as outras verificações podem ser ignoradas. A função também verifica sev
éinf
ounan
:Agora sabemos que, se
w
essas verificações falharem, não é um flutuador Python. Agora a função verifica se é um número inteiro Python. Se for esse o caso, o teste mais fácil é extrair o sinal dev
e o sinal dew
(retornar0
se zero,-1
se negativo,1
se positivo). Se os sinais forem diferentes, essas são todas as informações necessárias para retornar o resultado da comparação:Se esta verificação falhar, em seguida,
v
ew
têm o mesmo sinal.A próxima verificação conta o número de bits no número inteiro
w
. Se tiver muitos bits, não poderá ser mantido como um flutuador e, portanto, deve ser maior em magnitude do que o flutuadorv
:Por outro lado, se o número inteiro
w
tiver 48 ou menos bits, ele pode transformar com segurança um C duploj
e comparar:Desse ponto em diante, sabemos que
w
possui 49 ou mais bits. Será conveniente tratarw
como um número inteiro positivo; portanto, altere o sinal e o operador de comparação conforme necessário:Agora a função olha para o expoente do flutuador. Lembre-se de que um float pode ser gravado (sinal de ignição) como expoente significand * 2 e que o significand representa um número entre 0,5 e 1:
Isso verifica duas coisas. Se o expoente for menor que 0, o flutuador será menor que 1 (e, portanto, menor em magnitude que qualquer número inteiro). Ou, se o expoente for menor que o número de bits,
w
então temos que,v < |w|
já que o significante * 2 expoente é menor que 2 nbits .Na falta dessas duas verificações, a função procura ver se o expoente é maior que o número de bits
w
. Isso mostra que o significante * 2 expoente é maior que 2 nbits e, portantov > |w|
:Se essa verificação não for bem-sucedida, sabemos que o expoente da flutuação
v
é o mesmo que o número de bits no número inteirow
.A única maneira de comparar os dois valores agora é construir dois novos inteiros Python a partir de
v
ew
. A idéia é descartar a parte fracionária dev
, dobrar a parte inteira e adicionar uma.w
também é duplicado e esses dois novos objetos Python podem ser comparados para fornecer o valor de retorno correto. Usando um exemplo com valores pequenos,4.65 < 4
seria determinado pela comparação(2*4)+1 == 9 < 8 == (2*4)
(retornando false).Por uma questão de brevidade, deixei de lado a verificação de erros e o rastreamento de lixo adicionais que o Python precisa fazer quando cria esses novos objetos. Escusado será dizer que isso adiciona uma sobrecarga adicional e explica por que os valores destacados na pergunta são significativamente mais lentos em comparação do que outros.
Aqui está um resumo das verificações que são executadas pela função de comparação.
Let
v
Ser um float e lançá-lo como um C duplo. Agora, sew
também é um float:Verifique se
w
énan
ouinf
. Nesse caso, lide com este caso especial separadamente, dependendo do tipo dew
.Caso contrário, compare
v
ew
diretamente por suas representações, enquanto C dobra.Se
w
é um número inteiro:Extraia os sinais de
v
ew
. Se são diferentes, sabemosv
ew
somos diferentes e qual é o maior valor.( Os sinais são os mesmos. ) Verifique se
w
há bits demais para ser um flutuador (mais quesize_t
). Se sim,w
tem uma magnitude maior quev
.Verifique se
w
possui 48 ou menos bits. Nesse caso, ele pode ser convertido com segurança em um duplo C sem perder a precisão e comparado comv
.(
w
possui mais de 48 bits. Agora, trataremosw
como um número inteiro positivo depois de alterar a comparação, conforme apropriado. )Considere o expoente do flutuador
v
. Se o expoente for negativo,v
será menor que1
e, portanto, menor que qualquer número inteiro positivo. Casow
contrário , se o expoente for menor que o número de bits , deverá ser menor quew
.Se o expoente de
v
for maior que o número de bits,w
entãov
será maior quew
.( O expoente é igual ao número de bits
w
. )A verificação final. Divida
v
em partes inteiras e fracionárias. Dobre a parte inteira e adicione 1 para compensar a parte fracionária. Agora dobre o número inteirow
. Compare esses dois novos números inteiros para obter o resultado.fonte
Usando
gmpy2
com flutuadores de precisão arbitrários e números inteiros, é possível obter um desempenho de comparação mais uniforme:fonte
decimal
na biblioteca padrão