Mengapa array Python lambat?

153

Saya berharap array.arraylebih cepat dari daftar, karena array tampaknya tidak dikotak.

Namun, saya mendapatkan hasil berikut:

In [1]: import array

In [2]: L = list(range(100000000))

In [3]: A = array.array('l', range(100000000))

In [4]: %timeit sum(L)
1 loop, best of 3: 667 ms per loop

In [5]: %timeit sum(A)
1 loop, best of 3: 1.41 s per loop

In [6]: %timeit sum(L)
1 loop, best of 3: 627 ms per loop

In [7]: %timeit sum(A)
1 loop, best of 3: 1.39 s per loop

Apa yang bisa menjadi penyebab perbedaan seperti itu?

Valentin Lorentz
sumber
4
alat numpy dapat mengeksploitasi array Anda secara efisien:% timeit np.sum (A): 100 loop, terbaik 3: 8,87 ms per loop
BM
6
Saya tidak pernah menemukan situasi di mana saya harus menggunakan arraypaket. Jika Anda ingin melakukan sejumlah besar matematika, Numpy beroperasi dengan kecepatan cahaya (yaitu C), dan biasanya lebih baik daripada implementasi yang naif seperti sum()).
Nick T
40
Tutup pemilih: Mengapa sebenarnya ini berdasarkan pendapat? OP tampaknya mengajukan pertanyaan spesifik dan teknis tentang fenomena yang terukur dan berulang.
Kevin
5
@NickT Baca Anekdot optimisasi . Ternyata arraycukup cepat dalam mengubah string bilangan bulat (mewakili byte ASCII) ke strobjek. Guido sendiri hanya menemukan ini setelah banyak solusi lain dan cukup terkejut dengan kinerjanya. Bagaimanapun ini adalah satu-satunya tempat di mana saya ingat melihatnya bermanfaat. numpyjauh lebih baik untuk berurusan dengan array tetapi ketergantungan pihak ke-3.
Bakuriu

Jawaban:

220

The penyimpanan adalah "unboxed", tapi setiap kali Anda mengakses sebuah elemen Python harus "kotak" itu (menanamkan dalam sebuah objek Python biasa) untuk melakukan apa-apa dengan itu. Misalnya, Anda sum(A)mengulangi array, dan kotak setiap integer, satu per satu, dalam intobjek Python biasa . Itu menghabiskan waktu. Di Anda sum(L), semua tinju dilakukan pada saat daftar itu dibuat.

Jadi, pada akhirnya, sebuah array pada umumnya lebih lambat, tetapi membutuhkan memori yang jauh lebih sedikit.


Berikut kode yang relevan dari versi terbaru Python 3, tetapi ide dasar yang sama berlaku untuk semua implementasi CPython sejak Python pertama kali dirilis.

Berikut kode untuk mengakses item daftar:

PyObject *
PyList_GetItem(PyObject *op, Py_ssize_t i)
{
    /* error checking omitted */
    return ((PyListObject *)op) -> ob_item[i];
}

Ada sangat sedikit untuk itu: somelist[i]hanya mengembalikan iobjek th dalam daftar (dan semua objek Python di CPython adalah pointer ke sebuah struct yang segmen awal sesuai dengan tata letak a struct PyObject).

Dan inilah __getitem__implementasi untuk arraykode tipel :

static PyObject *
l_getitem(arrayobject *ap, Py_ssize_t i)
{
    return PyLong_FromLong(((long *)ap->ob_item)[i]);
}

Memori mentah diperlakukan sebagai vektor C longbilangan bulat platform-asli ; yang i' C longdibaca'; dan kemudian PyLong_FromLong()dipanggil untuk membungkus ("kotak") asli C longdalam longobjek Python (yang, dalam Python 3, yang menghilangkan perbedaan Python 2 antaraint dan long, sebenarnya ditampilkan sebagai tipe int).

Tinju ini harus mengalokasikan memori baru untuk intobjek Python , dan menyemprotkan aslinyaC long bit asli ke dalamnya. Dalam konteks contoh asli, masa hidup objek ini sangat singkat (hanya cukup lama untuk sum()menambahkan konten ke total berjalan), dan kemudian lebih banyak waktu diperlukan untuk membatalkan alokasi baruint objek .

Di sinilah perbedaan kecepatan berasal, selalu datang, dan selalu akan datang dari dalam implementasi CPython.

Tim Peters
sumber
87

Untuk menambah jawaban sempurna Tim Peters, array mengimplementasikan protokol buffer , sementara daftar tidak. Ini berarti bahwa, jika Anda menulis ekstensi C (atau persamaan moral, seperti menulis modul Cython ), maka Anda dapat mengakses dan bekerja dengan elemen-elemen array jauh lebih cepat daripada apa pun yang bisa dilakukan Python. Ini akan memberi Anda peningkatan kecepatan yang besar, mungkin jauh di atas urutan besarnya. Namun, ada beberapa kelemahan:

  1. Anda sekarang dalam bisnis menulis C bukan Python. Cython adalah salah satu cara untuk memperbaiki ini, tetapi tidak menghilangkan banyak perbedaan mendasar antara bahasa; Anda harus terbiasa dengan semantik C dan memahami apa yang dilakukannya.
  2. API C PyPy bekerja sampai batas tertentu , tetapi tidak terlalu cepat. Jika Anda menargetkan PyPy, Anda mungkin harus menulis kode sederhana dengan daftar biasa, dan kemudian biarkan JITter mengoptimalkannya untuk Anda.
  3. Ekstensi C lebih sulit untuk didistribusikan daripada kode Python murni karena harus dikompilasi. Kompilasi cenderung bergantung pada arsitektur dan sistem operasi, jadi Anda perlu memastikan Anda mengkompilasi untuk platform target Anda.

Langsung ke ekstensi C mungkin menggunakan palu godam untuk memukul lalat, tergantung pada kasus penggunaan Anda. Anda harus terlebih dahulu menyelidiki NumPy dan melihat apakah itu cukup kuat untuk melakukan matematika apa pun yang Anda coba lakukan. Ini juga akan jauh lebih cepat daripada Python asli, jika digunakan dengan benar.

Kevin
sumber
10

Tim Peters menjawab mengapa ini lambat, tetapi mari kita lihat bagaimana memperbaikinya .

Menempel pada contoh Anda sum(range(...))(faktor 10 lebih kecil dari contoh Anda agar masuk ke memori di sini):

import numpy
import array
L = list(range(10**7))
A = array.array('l', L)
N = numpy.array(L)

%timeit sum(L)
10 loops, best of 3: 101 ms per loop

%timeit sum(A)
1 loop, best of 3: 237 ms per loop

%timeit sum(N)
1 loop, best of 3: 743 ms per loop

Dengan cara ini juga perlu numpy ke kotak / unbox, yang memiliki overhead tambahan. Untuk membuatnya cepat, seseorang harus tetap dalam kode c numpy:

%timeit N.sum()
100 loops, best of 3: 6.27 ms per loop

Jadi dari daftar solusi ke versi numpy ini adalah faktor 16 dalam runtime.

Mari kita periksa juga berapa lama membuat struktur data tersebut

%timeit list(range(10**7))
1 loop, best of 3: 283 ms per loop

%timeit array.array('l', range(10**7))
1 loop, best of 3: 884 ms per loop

%timeit numpy.array(range(10**7))
1 loop, best of 3: 1.49 s per loop

%timeit numpy.arange(10**7)
10 loops, best of 3: 21.7 ms per loop

Hapus pemenang: Numpy

Juga perhatikan bahwa membuat struktur data membutuhkan waktu sebanyak penjumlahan, jika tidak lebih. Mengalokasikan memori lambat.

Penggunaan memori dari mereka:

sys.getsizeof(L)
90000112
sys.getsizeof(A)
81940352
sys.getsizeof(N)
80000096

Jadi ini membutuhkan 8 byte per angka dengan berbagai overhead. Untuk range kita menggunakan int 32bit sudah mencukupi, jadi kita bisa mengamankan sebagian memori.

N=numpy.arange(10**7, dtype=numpy.int32)

sys.getsizeof(N)
40000096

%timeit N.sum()
100 loops, best of 3: 8.35 ms per loop

Tapi ternyata menambahkan int 64bit lebih cepat dari int 32bit di mesin saya, jadi ini hanya layak jika Anda dibatasi oleh memori / bandwidth.

Robin Roth
sumber
-1

harap dicatat bahwa 100000000sama dengan 10^8tidak 10^7, dan hasil saya adalah sebagai berikut:

100000000 == 10**8

# my test results on a Linux virtual machine:
#<L = list(range(100000000))> Time: 0:00:03.263585
#<A = array.array('l', range(100000000))> Time: 0:00:16.728709
#<L = list(range(10**8))> Time: 0:00:03.119379
#<A = array.array('l', range(10**8))> Time: 0:00:18.042187
#<A = array.array('l', L)> Time: 0:00:07.524478
#<sum(L)> Time: 0:00:01.640671
#<np.sum(L)> Time: 0:00:20.762153
S. Cheraghifar
sumber