Dlaczego x ** 4.0 jest szybszy niż x ** 4 w Pythonie 3?

164

Dlaczego jest x**4.0szybszy niż x**4? Używam 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

Próbowałem zmienić siłę, o którą podniosłem, aby zobaczyć, jak to działa, i na przykład jeśli podbiję x do potęgi 10 lub 16, przeskakuje z 30 do 35, ale jeśli podbijam o 10,0 jako float, po prostu się porusza około 24,1 ~ 4.

Myślę, że ma to coś wspólnego z konwersją typu float i może z potęgami 2, ale tak naprawdę nie wiem.

Zauważyłem, że w obu przypadkach potęgi 2 są szybsze, myślę, że te obliczenia są bardziej natywne / łatwe dla interpretera / komputera. Ale mimo to, z pływakami, prawie się nie porusza. 2.0 => 24.1~4 & 128.0 => 24.1~4 ale 2 => 29 & 128 => 62


TigerhawkT3 wskazał, że nie dzieje się to poza pętlą. Sprawdziłem i sytuacja ma miejsce (z tego co widziałem) tylko wtedy, gdy podstawa się podnosi. Masz o tym jakiś pomysł?

arieljannai
źródło
11
Co jest warte: Python 2.7.13 jest dla mnie o współczynnik 2 ~ 3 szybszy i wykazuje odwrotne zachowanie: wykładnik całkowity jest szybszy niż wykładnik zmiennoprzecinkowy.
4
@Evert tak, mam 14 usek dla x**4.0i 3,9 dla x**4.
dabadaba

Odpowiedzi:

161

Dlaczego jest x**4.0 szybszy niż x**4w Pythonie 3 * ?

intObiekty Pythona 3 są pełnoprawnymi obiektami zaprojektowanymi do obsługi dowolnego rozmiaru; w związku z tym są traktowane jako takie na poziomie C (zobacz, jak wszystkie zmienne są deklarowane jako PyLongObject *typ in long_pow). To również sprawia, że ​​ich potęgowanie jest znacznie trudniejsze i bardziej żmudne, ponieważ musisz bawić się ob_digittablicą, której używa do reprezentowania jej wartości, aby ją wykonać. ( Źródło dla odważnych. - Zobacz: Zrozumienie alokacji pamięci dla dużych liczb całkowitych w Pythonie, aby uzyskać więcej informacji na temat PyLongObjects.)

floatWręcz przeciwnie, obiekty Pythona można przekształcić do doubletypu C (przy użyciu PyFloat_AsDouble), a operacje można wykonywać przy użyciu tych typów natywnych . To jest wielki , ponieważ po sprawdzeniu odpowiednich krawędziowych przypadkach pozwala Pythona do korzystania z platformypow ( C użytkownika pow, że jest ), aby obsłużyć rzeczywisty potęgowanie:

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

gdzie ivi iwsą naszymi oryginałami PyFloatObjectjako C doubles.

Bo to jest warte: Python jest 2.7.13dla mnie czynnikiem 2~3szybszym i wykazuje odwrotne zachowanie.

Poprzedni fakt wyjaśnia również rozbieżność między Pythonem 2 i 3, więc pomyślałem, że odniosę się również do tego komentarza, ponieważ jest interesujący.

W Pythonie 2 używasz starego intobiektu, który różni się od intobiektu w Pythonie 3 (wszystkie intobiekty w 3.x są PyLongObjecttypu). W Pythonie 2 istnieje różnica, która zależy od wartości obiektu (lub, jeśli używasz przyrostka L/l):

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

To, <type 'int'>co widzisz tutaj, robi to samo, co floatrobi , jest bezpiecznie konwertowane na C, long gdy wykonywane jest na nim potęgowanie ( int_powpodpowiada również kompilatorowi, aby umieścić je w rejestrze, jeśli może to zrobić, więc może to zrobić różnicę) :

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

pozwala to na dobry przyrost prędkości.

Aby zobaczyć, jak powolne <type 'long'>są s w porównaniu do <type 'int'>s, jeśli umieścisz xnazwę w longwywołaniu w Pythonie 2 (zasadniczo zmuszając ją do użycia long_powjak w Pythonie 3), przyrost prędkości znika:

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

Należy wziąć pod uwagę, że choć jeden snippet przekształca się intdo longpodczas gdy inne nie (jak podkreślił @pydsinger), to obsada nie jest przyczynianie się siłą spowolnienia. Wdrożenie long_powis. (Zmierz czas tylko z wyrażeniami, long(x)aby zobaczyć).

[…] nie dzieje się to poza pętlą. […] Masz o tym jakiś pomysł?

To jest optymalizator wizualizacji CPythona składający stałe za Ciebie. W obu przypadkach otrzymujesz te same dokładne czasy, ponieważ nie ma rzeczywistych obliczeń, które pozwolą znaleźć wynik potęgowania, tylko ładowanie wartości:

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

Generowany jest identyczny kod bajtowy, '4 ** 4.'z tą różnicą, że LOAD_CONSTładuje zmiennoprzecinkowy 256.0zamiast 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

Więc czasy są identyczne.


* Wszystkie powyższe dotyczą wyłącznie CPythona, referencyjnej implementacji Pythona. Inne implementacje mogą działać inaczej.

Dimitris Fasarakis Hilliard
źródło
Cokolwiek to jest, jest związane z pętlą po a range, ponieważ tylko **sama operacja nie daje różnicy między liczbami całkowitymi i zmiennoprzecinkowymi.
TigerhawkT3
Różnica pojawia się tylko wtedy, gdy wyszukuje się zmienną ( 4**4jest tak samo szybka jak 4**4.0), a ta odpowiedź w ogóle tego nie dotyka.
TigerhawkT3
1
Ale stałe zostaną zwinięte @ TigerhawkT3 ( dis(compile('4 ** 4', '', 'exec'))), więc czas powinien być dokładnie taki sam.
Dimitris Fasarakis Hilliard
Wydaje się, że Twoje ostatnie czasy nie pokazują tego, co mówisz. long(x)**2.jest nadal szybsza niż long(x)**24-5 razy. (Jednak nie jeden z przeciwników)
Graipher
3
@ mbomb007 eliminacja <type 'long'>typu w Pythonie 3 jest prawdopodobnie wyjaśniona przez wysiłki podjęte w celu uproszczenia języka. Jeśli możesz mieć jeden typ do reprezentowania liczb całkowitych, jest to łatwiejsze do zarządzania niż dwa (i martwienie się o konwersję z jednego na drugi, gdy to konieczne, zdezorientowanie użytkowników itp.) Przyrost prędkości ma drugorzędne znaczenie. Część dotycząca uzasadnienia w PEP 237 również oferuje więcej informacji.
Dimitris Fasarakis Hilliard
25

Jeśli spojrzymy na kod bajtowy, zobaczymy, że wyrażenia są całkowicie identyczne. Jedyną różnicą jest rodzaj stałej, która będzie argumentem BINARY_POWER. Więc z całą pewnością jest to intspowodowane konwersją na liczbę zmiennoprzecinkową w dół wiersza.

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

Aktualizacja: spójrzmy na Objects / abstract.c w kodzie źródłowym CPython:

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

PyNumber_Powerpołączeń ternary_op, które są zbyt długie, aby je wkleić, więc oto link .

Wywołuje nb_powerprzedział x, przekazując yjako argument.

Wreszcie w float_pow()wierszu 686 Objects / floatobject.c widzimy, że argumenty są konwertowane na C doubletuż przed samą operacją:

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
źródło
1
@ Jean-FrançoisFabre Uważam, że jest to spowodowane ciągłym składaniem.
Dimitris Fasarakis Hilliard
2
Myślę, że implikacja, że ​​istnieje konwersja i nie są obsługiwane inaczej w dół linii "z pewnością" jest trochę naciągnięta bez źródła.
miradulo
1
@Mitch - Zwłaszcza, że ​​w tym konkretnym kodzie nie ma różnicy w czasie wykonywania tych dwóch operacji. Różnica pojawia się tylko w przypadku pętli OP. Ta odpowiedź prowadzi do wniosków.
TigerhawkT3
2
Dlaczego patrzysz tylko float_powwtedy, gdy to nawet nie działa w powolnym przypadku?
user2357112 obsługuje Monikę
2
@ TigerhawkT3: 4**4i spasuj na 4**4.0stałe. To zupełnie odrębny efekt.
user2357112 obsługuje Monikę
-1

Ponieważ jeden jest poprawny, drugi jest przybliżeniem.

>>> 334453647687345435634784453567231654765 ** 4.0
1.2512490121794596e+154
>>> 334453647687345435634784453567231654765 ** 4
125124901217945966595797084130108863452053981325370920366144
719991392270482919860036990488994139314813986665699000071678
41534843695972182197917378267300625
Veky
źródło
Nie wiem, dlaczego ten przeciwnik przegłosował, ale zrobiłem to, ponieważ ta odpowiedź nie odpowiada na pytanie. Tylko dlatego, że coś jest poprawne, w żaden sposób nie oznacza, że ​​jest szybsze lub wolniejsze. Jeden jest wolniejszy niż drugi, ponieważ jeden może pracować z typami C, podczas gdy drugi musi pracować z obiektami Pythona.
Dimitris Fasarakis Hilliard
1
Dziękuję za wyjaśnienie. Cóż, naprawdę pomyślałem, że to oczywiste, że szybciej jest obliczyć przybliżenie liczby do około 12 cyfr, niż obliczyć je wszystkie dokładnie. W końcu jedynym powodem, dla którego używamy przybliżeń, jest to, że są one szybsze do obliczenia, prawda?
Veky