Mengapa x ** 4.0 lebih cepat dari x ** 4 di Python 3?

164

Kenapa x**4.0lebih cepat dari itu x**4? Saya menggunakan 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

Saya mencoba mengubah kekuatan yang saya naikkan untuk melihat bagaimana kerjanya, dan misalnya jika saya menaikkan x ke kekuatan 10 atau 16, ia melonjak dari 30 menjadi 35, tetapi jika saya menaikkan sebesar 10,0 sebagai pelampung, itu hanya bergerak sekitar 24.1 ~ 4.

Saya kira itu ada hubungannya dengan konversi float dan kekuatan 2 mungkin, tapi saya tidak benar-benar tahu.

Saya perhatikan bahwa dalam kedua kasus, kekuatan 2 lebih cepat, saya kira karena perhitungan tersebut lebih asli / mudah bagi juru bahasa / komputer. Tapi tetap saja, dengan mengapung itu hampir tidak bergerak. 2.0 => 24.1~4 & 128.0 => 24.1~4 tapi 2 => 29 & 128 => 62


TigerhawkT3 menunjukkan bahwa itu tidak terjadi di luar loop. Saya memeriksa dan situasinya hanya terjadi (dari apa yang saya lihat) ketika pangkalan dinaikkan. Ada ide tentang itu?

arieljannai
sumber
11
Untuk apa nilainya: Python 2.7.13 bagi saya adalah faktor 2 ~ 3 lebih cepat, dan menunjukkan perilaku terbalik: eksponen integer lebih cepat daripada eksponen floating point.
4
@ Balik ya, saya mendapat 14 usec untuk x**4.0dan 3,9 untuk x**4.
dabadaba

Jawaban:

161

Mengapa x**4.0 lebih cepat daripada x**4di Python 3 * ?

intObjek Python 3 adalah objek penuh yang dirancang untuk mendukung ukuran sewenang-wenang; karena fakta itu, mereka ditangani seperti itu pada level C (lihat bagaimana semua variabel dinyatakan sebagai PyLongObject *tipe long_pow). Ini juga membuat eksponensial mereka jauh lebih rumit dan melelahkan karena Anda perlu bermain-main dengan ob_digitarray yang digunakannya untuk mewakili nilainya untuk melakukannya. ( Sumber untuk pemberani. - Lihat: Memahami alokasi memori untuk bilangan bulat besar di Python untuk informasi lebih lanjut PyLongObject.)

floatObjek Python , sebaliknya, dapat ditransformasikan ke doubletipe C (dengan menggunakan PyFloat_AsDouble) dan operasi dapat dilakukan dengan menggunakan tipe-tipe asli tersebut . Ini bagus karena, setelah memeriksa tepi-kasus yang relevan, memungkinkan Python untuk menggunakan platformpow ( C pow, yaitu ) untuk menangani eksponensial yang sebenarnya:

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

di mana ivdan iwasli PyFloatObjects sebagai C doubles.

Untuk apa nilainya: Python 2.7.13bagi saya adalah faktor yang 2~3lebih cepat, dan menunjukkan perilaku terbalik.

Fakta sebelumnya juga menjelaskan perbedaan antara Python 2 dan 3 jadi, saya pikir saya akan membahas komentar ini juga karena itu menarik.

Di Python 2, Anda menggunakan intobjek lama yang berbeda dari intobjek di Python 3 (semua intobjek dalam 3.x PyLongObjectbertipe). Di Python 2, ada perbedaan yang tergantung pada nilai objek (atau, jika Anda menggunakan akhiran L/l):

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

The <type 'int'>Anda lihat di sini melakukan hal yang sama floats lakukan , itu akan aman diubah menjadi C long ketika eksponensial dilakukan di atasnya (yang int_powjuga mengisyaratkan compiler untuk menempatkan 'em di register jika dapat melakukannya, sehingga bisa membuat perbedaan) :

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

ini memungkinkan untuk mendapatkan kecepatan yang baik.

Untuk melihat bagaimana lambannya <type 'long'>s dibandingkan dengan <type 'int'>s, jika Anda membungkus xnama dalam longpanggilan dengan Python 2 (pada dasarnya memaksanya untuk digunakan long_powseperti pada Python 3), kenaikan kecepatan menghilang:

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

Perhatikan bahwa, meskipun satu cuplikan mengubah intke longsementara yang lain tidak (seperti yang ditunjukkan oleh @pydsinger), pemeran ini bukanlah kekuatan penyumbang di balik perlambatan. Implementasinya long_powadalah. (Hitung waktu pernyataan hanya dengan long(x)melihat).

[...] itu tidak terjadi di luar loop. [...] Ada ide tentang itu?

Ini adalah pengoptimal lubang intip CPython melipat konstanta untuk Anda. Anda mendapatkan ketepatan waktu yang sama, karena tidak ada perhitungan yang sebenarnya untuk menemukan hasil eksponensial, hanya memuat nilai:

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

Kode byte identik dihasilkan '4 ** 4.'dengan satu-satunya perbedaan adalah bahwa LOAD_CONSTmemuat float, 256.0bukan 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

Jadi waktunya identik.


* Semua hal di atas hanya berlaku untuk CPython, implementasi referensi dari Python. Implementasi lain mungkin berkinerja berbeda.

Dimitris Fasarakis Hilliard
sumber
Apa pun itu, ini terkait dengan loop di atas range, karena hanya pengaturan waktu **operasi itu sendiri tidak menghasilkan perbedaan antara bilangan bulat dan mengapung.
TigerhawkT3
Perbedaannya hanya muncul ketika mencari variabel ( 4**4hanya secepat 4**4.0), dan jawaban ini tidak menyentuh itu sama sekali.
TigerhawkT3
1
Tapi, konstanta akan dilipat @ TigerhawkT3 ( dis(compile('4 ** 4', '', 'exec'))) sehingga waktunya harus persis sama.
Dimitris Fasarakis Hilliard
Pengaturan waktu terakhir Anda sepertinya tidak menunjukkan apa yang Anda katakan. long(x)**2.masih lebih cepat daripada long(x)**2dengan faktor 4-5. (Bukan salah satu dari downvoters)
Graipher
3
@ mbomb007 penghapusan <type 'long'>jenis Python 3 mungkin dijelaskan oleh upaya yang dilakukan untuk menyederhanakan bahasa. Jika Anda dapat memiliki satu jenis untuk mewakili bilangan bulat, itu lebih mudah dikelola daripada dua (dan khawatir tentang konversi dari satu ke yang lain bila perlu, pengguna menjadi bingung dll). Gain kecepatan adalah sekunder dari itu. Bagian pemikiran PEP 237 juga menawarkan beberapa wawasan lebih banyak.
Dimitris Fasarakis Hilliard
25

Jika kita melihat bytecode, kita dapat melihat bahwa ekspresi itu murni identik. Satu-satunya perbedaan adalah jenis konstanta yang akan menjadi argumen BINARY_POWER. Jadi itu pasti karena intsedang dikonversi ke nomor floating point di telepon.

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

Pembaruan: mari kita lihat Objects / abstract.c dalam kode sumber CPython:

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

PyNumber_Powerpanggilan ternary_op, yang terlalu panjang untuk ditempelkan di sini, jadi inilah tautannya .

Itu panggilan nb_powerslot x, lewat ysebagai argumen.

Akhirnya, float_pow()pada baris 686 dari Objects / floatobject.c kita melihat bahwa argumen dikonversikan menjadi C doubletepat sebelum operasi aktual:

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
sumber
1
@ Jean-FrançoisFabre Saya percaya itu karena lipatan yang konstan.
Dimitris Fasarakis Hilliard
2
Saya pikir implikasi bahwa ada konversi dan mereka tidak ditangani secara berbeda di garis "pasti" adalah sedikit peregangan tanpa sumber.
miradulo
1
@Mitch - Terutama karena, dalam kode khusus ini, tidak ada perbedaan dalam waktu eksekusi untuk kedua operasi tersebut. Perbedaannya hanya muncul dengan loop OP. Jawaban ini melompat ke kesimpulan.
TigerhawkT3
2
Mengapa Anda hanya melihat float_powketika itu bahkan tidak berjalan untuk kasing lambat?
user2357112 mendukung Monica
2
@ TigerhawkT3: 4**4dan 4**4.0dapatkan lipatan konstan. Itu adalah efek yang sepenuhnya terpisah.
user2357112 mendukung Monica
-1

Karena yang satu benar, yang lain adalah aproksimasi.

>>> 334453647687345435634784453567231654765 ** 4.0
1.2512490121794596e+154
>>> 334453647687345435634784453567231654765 ** 4
125124901217945966595797084130108863452053981325370920366144
719991392270482919860036990488994139314813986665699000071678
41534843695972182197917378267300625
Veky
sumber
Saya tidak tahu mengapa downvoter itu kalah, tetapi saya melakukannya karena jawaban ini tidak menjawab pertanyaan. Hanya karena sesuatu itu benar tidak berarti ia lebih cepat atau lebih lambat. Satu lebih lambat dari yang lain karena satu dapat bekerja dengan tipe C sementara yang lain harus bekerja dengan Objek Python.
Dimitris Fasarakis Hilliard
1
Terima kasih untuk penjelasannya. Yah, saya benar-benar berpikir itu jelas bahwa lebih cepat untuk menghitung hanya perkiraan angka menjadi 12 atau lebih digit, daripada menghitung semuanya secara tepat. Lagipula, satu-satunya alasan mengapa kita menggunakan perkiraan adalah karena lebih cepat untuk dihitung, bukan?
Veky