Por que x ** 4.0 é mais rápido que x ** 4 no Python 3?

164

Por que é x**4.0mais rápido que x**4? Estou usando o CPython 3.5.2.

$ python -m timeit "for x in range(100):" " x**4.0"
  10000 loops, best of 3: 24.2 usec per loop

$ python -m timeit "for x in range(100):" " x**4"
  10000 loops, best of 3: 30.6 usec per loop

Tentei mudar o poder que levantei para ver como ele funciona e, por exemplo, se eu aumentar x para o poder de 10 ou 16, ele está saltando de 30 para 35, mas se estou aumentando em 10,0 como flutuador, está apenas se movendo em torno de 24,1 ~ 4.

Eu acho que tem algo a ver com conversão de float e potências de 2, talvez, mas eu realmente não sei.

Notei que, em ambos os casos, potências de 2 são mais rápidas, acho que, já que esses cálculos são mais nativos / fáceis para o intérprete / computador. Mas ainda assim, com carros alegóricos quase não está se movendo. 2.0 => 24.1~4 & 128.0 => 24.1~4 mas 2 => 29 & 128 => 62


TigerhawkT3 apontou que isso não acontece fora do circuito. Eu verifiquei e a situação só ocorre (pelo que vi) quando a base está aumentando. Alguma ideia sobre isso?

arieljannai
fonte
11
Pelo que vale: Python 2.7.13 para mim é um fator 2 a 3 mais rápido e mostra o comportamento inverso: um expoente inteiro é mais rápido que um expoente de ponto flutuante.
4
@ Yvert Ever, eu tenho 14 usec para x**4.0e 3,9 para x**4.
dabadaba

Respostas:

161

Por que é x**4.0 mais rápido que x**4no Python 3 * ?

Os intobjetos Python 3 são um objeto completo desenvolvido para suportar um tamanho arbitrário; devido a esse fato, elas são tratadas como tal no nível C (veja como todas as variáveis ​​são declaradas como PyLongObject *tipo long_pow). Isso também torna a exponenciação muito mais complicada e tediosa, pois você precisa brincar com o ob_digitarray que ele usa para representar seu valor para executá-lo. ( Fonte para os corajosos. - Consulte: Entendendo a alocação de memória para números inteiros grandes em Python para obter mais informações sobre PyLongObjects.)

Os floatobjetos Python , pelo contrário, podem ser transformados em um doubletipo C (usando PyFloat_AsDouble) e as operações podem ser executadas usando esses tipos nativos . Isso é ótimo , porque, após a verificação de borda casos relevantes, permite Python para usar as plataformaspow ( de C pow, que é ) para lidar com a exponenciação real:

/* Now iv and iw are finite, iw is nonzero, and iv is
 * positive and not equal to 1.0.  We finally allow
 * the platform pow to step in and do the rest.
 */
errno = 0;
PyFPE_START_PROTECT("pow", return NULL)
ix = pow(iv, iw); 

Onde ive quais iwsão os nossos PyFloatObjects originais como C doubles.

Pelo que vale a pena: Python 2.7.13para mim é um fator 2~3mais rápido e mostra o comportamento inverso.

O fato anterior também explica a discrepância entre Python 2 e 3, então, pensei em abordar esse comentário também porque é interessante.

No Python 2, você está usando o intobjeto antigo que difere do intobjeto no Python 3 (todos os intobjetos no 3.x são do PyLongObjecttipo). No Python 2, há uma distinção que depende do valor do objeto (ou, se você usar o sufixo L/l):

# Python 2
type(30)  # <type 'int'>
type(30L) # <type 'long'>

O <type 'int'>que você vê aqui faz a mesma coisa floats fazer , ele é convertido com segurança em um C long quando exponenciação é realizada sobre ele (o int_powtambém sugere que o compilador para colocá-los num registo se ele pode fazê-lo, de modo que poderia fazer a diferença) :

static PyObject *
int_pow(PyIntObject *v, PyIntObject *w, PyIntObject *z)
{
    register long iv, iw, iz=0, ix, temp, prev;
/* Snipped for brevity */    

isso permite um bom ganho de velocidade.

Para ver como <type 'long'>s são lentos em comparação com <type 'int'>s, se você colocar o xnome em uma longchamada no Python 2 (essencialmente forçando-o a usar long_powcomo no Python 3), o ganho de velocidade desaparece:

# <type 'int'>
(python2)  python -m timeit "for x in range(1000):" " x**2"       
10000 loops, best of 3: 116 usec per loop
# <type 'long'> 
(python2)  python -m timeit "for x in range(1000):" " long(x)**2"
100 loops, best of 3: 2.12 msec per loop

Observe que, embora um trecho transforme o intto longenquanto o outro não (como apontado por @pydsinger), esse elenco não é a força que contribui por trás da desaceleração. A implementação de long_powé. (Cronometre as declarações apenas com long(x)para ver).

[...] isso não acontece fora do loop. [...] tem ideia disso?

Este é o otimizador de olho mágico do CPython, dobrando as constantes para você. Você obtém os mesmos tempos exatos em ambos os casos, pois não há computação real para encontrar o resultado da exponenciação, apenas o carregamento de valores:

dis.dis(compile('4 ** 4', '', 'exec'))
  1           0 LOAD_CONST               2 (256)
              3 POP_TOP
              4 LOAD_CONST               1 (None)
              7 RETURN_VALUE

O código de byte idêntico é gerado, '4 ** 4.'com a única diferença: o LOAD_CONSTcarregamento do float em 256.0vez do int 256:

dis.dis(compile('4 ** 4.', '', 'exec'))
  1           0 LOAD_CONST               3 (256.0)
              2 POP_TOP
              4 LOAD_CONST               2 (None)
              6 RETURN_VALUE

Então os tempos são idênticos.


* Todas as opções acima se aplicam apenas ao CPython, a implementação de referência do Python. Outras implementações podem ter um desempenho diferente.

Dimitris Fasarakis Hilliard
fonte
Seja o que for, está relacionado ao loop sobre a range, pois somente a temporização da **operação não produz diferença entre números inteiros e flutuantes.
TigerhawkT3
A diferença só aparece ao procurar uma variável ( 4**4é tão rápida quanto 4**4.0), e essa resposta não afeta nada disso.
precisa saber é o seguinte
1
Porém, as constantes serão dobradas em @ TigerhawkT3 ( dis(compile('4 ** 4', '', 'exec'))), portanto o horário deve ser exatamente o mesmo.
Dimitris Fasarakis Hilliard
Seus últimos horários parecem não mostrar o que você diz. long(x)**2.ainda é mais rápido do que long(x)**2por um fator de 4-5. (
Porém
3
@ mbomb007 a eliminação do <type 'long'>tipo no Python 3 é provavelmente explicada pelos esforços feitos para simplificar a linguagem. Se você pode ter um tipo para representar números inteiros, é mais gerenciável que dois (e se preocupa em converter de um para outro quando necessário, usuários ficando confusos etc.). O ganho de velocidade é secundário a isso. A seção de justificativa do PEP 237 também oferece mais algumas dicas.
Dimitris Fasarakis Hilliard 21/02
25

Se olharmos para o bytecode, podemos ver que as expressões são puramente idênticas. A única diferença é um tipo de constante que será um argumento de BINARY_POWER. Portanto, é certamente devido a uma intconversão para um número de ponto flutuante na linha.

>>> def func(n):
...    return n**4
... 
>>> def func1(n):
...    return n**4.0
... 
>>> from dis import dis
>>> dis(func)
  2           0 LOAD_FAST                0 (n)
              3 LOAD_CONST               1 (4)
              6 BINARY_POWER
              7 RETURN_VALUE
>>> dis(func1)
  2           0 LOAD_FAST                0 (n)
              3 LOAD_CONST               1 (4.0)
              6 BINARY_POWER
              7 RETURN_VALUE

Atualização: vamos dar uma olhada em Objects / abstract.c no código fonte do CPython:

PyObject *
PyNumber_Power(PyObject *v, PyObject *w, PyObject *z)
{
    return ternary_op(v, w, z, NB_SLOT(nb_power), "** or pow()");
}

PyNumber_Powerchamadas ternary_op, que é muito longo para colar aqui, então aqui está o link .

Ele chama o nb_powerslot de x, passando ycomo argumento.

Finalmente, na float_pow()linha 686 de Objects / floatobject.c , vemos que os argumentos são convertidos em um C doubleantes da operação real:

static PyObject *
float_pow(PyObject *v, PyObject *w, PyObject *z)
{
    double iv, iw, ix;
    int negate_result = 0;

    if ((PyObject *)z != Py_None) {
        PyErr_SetString(PyExc_TypeError, "pow() 3rd argument not "
            "allowed unless all arguments are integers");
        return NULL;
    }

    CONVERT_TO_DOUBLE(v, iv);
    CONVERT_TO_DOUBLE(w, iw);
    ...
leovp
fonte
1
@ Jean-FrançoisFabre Acredito que isso se deva a dobramentos constantes.
Dimitris Fasarakis Hilliard
2
Eu acho que a implicação de que existe uma conversão e eles não são tratados de maneira diferente "certamente" é um pouco exagerado sem uma fonte.
21717 miradulo
1
@Itch - Particularmente porque, neste código em particular, não há diferença no tempo de execução para essas duas operações. A diferença só surge com o loop do OP. Esta resposta está chegando a conclusões.
TigerhawkT3
2
Por que você está olhando apenas float_powquando isso nem corre para o caso lento?
user2357112 suporta Monica
2
@ TigerhawkT3: 4**4e 4**4.0fique dobrado constantemente. Esse é um efeito totalmente separado.
user2357112 suporta Monica
-1

Porque um está correto, outro é aproximação.

>>> 334453647687345435634784453567231654765 ** 4.0
1.2512490121794596e+154
>>> 334453647687345435634784453567231654765 ** 4
125124901217945966595797084130108863452053981325370920366144
719991392270482919860036990488994139314813986665699000071678
41534843695972182197917378267300625
Veky
fonte
Não sei por que esse voto negativo foi votado, mas o fiz porque essa resposta não responde à pergunta. Só porque algo está correto não significa que seja mais rápido ou mais lento. Um é mais lento que o outro, porque um pode trabalhar com tipos C, enquanto o outro precisa trabalhar com objetos Python.
Dimitris Fasarakis Hilliard
1
Obrigada pelo esclarecimento. Bem, eu realmente pensei que era óbvio que é mais rápido calcular apenas a aproximação de um número a 12 ou mais dígitos, do que calcular todos eles exatamente. Afinal, a única razão pela qual usamos aproximações é que elas são mais rápidas de calcular, certo?
Veky