Saya baru-baru ini membandingkan kecepatan pemrosesan []
dan list()
dan terkejut menemukan bahwa []
berjalan lebih dari tiga kali lebih cepat daripada list()
. Saya menjalankan tes yang sama dengan {}
dan dict()
dan hasilnya praktis identik: []
dan {}
keduanya mengambil sekitar 0,128 detik / juta siklus, sementara list()
dan dict()
mengambil sekitar 0,428 detik / juta siklus masing-masing.
Kenapa ini? Apakah []
dan {}
(dan mungkin ()
dan ''
, juga) segera lulus kembali salinan dari beberapa literal saham kosong sementara rekan-rekan mereka secara eksplisit bernama ( list()
, dict()
, tuple()
, str()
) sepenuhnya pergi tentang menciptakan sebuah objek, apakah mereka benar-benar memiliki unsur-unsur?
Saya tidak tahu bagaimana kedua metode ini berbeda tetapi saya ingin mengetahuinya. Saya tidak dapat menemukan jawaban di dokumen atau di SO, dan mencari kurung kosong ternyata lebih bermasalah dari yang saya duga.
Saya mendapatkan hasil pengaturan waktu saya dengan menelepon timeit.timeit("[]")
dan timeit.timeit("list()")
, timeit.timeit("{}")
dan timeit.timeit("dict()")
, dan, masing-masing untuk membandingkan daftar dan kamus. Saya menjalankan Python 2.7.9.
Baru-baru ini saya menemukan " Mengapa jika Benar lebih lambat daripada jika 1? " Yang membandingkan kinerja if True
to if 1
dan tampaknya menyentuh skenario literal-versus-global yang serupa; mungkin perlu dipertimbangkan juga.
sumber
()
dan''
istimewa, karena tidak hanya kosong, mereka juga tidak berubah, dan karenanya, mudah untuk membuatnya menjadi lajang; mereka bahkan tidak membuat objek baru, hanya memuat singleton untuk yang kosongtuple
/str
. Secara teknis detail implementasi, tapi saya kesulitan membayangkan mengapa mereka tidak melakukan cache kosongtuple
/str
untuk alasan kinerja. Jadi intuisi Anda tentang[]
dan{}
mengembalikan stok literal adalah salah, tetapi itu berlaku untuk()
dan''
.{}
lebih cepat daripada meneleponset()
?Jawaban:
Karena
[]
dan{}
merupakan sintaksis literal . Python dapat membuat bytecode hanya untuk membuat daftar atau objek kamus:list()
dandict()
merupakan objek yang terpisah. Nama-nama mereka perlu diatasi, tumpukan harus dilibatkan untuk mendorong argumen, frame harus disimpan untuk mengambil nanti, dan panggilan harus dibuat. Itu semua membutuhkan lebih banyak waktu.Untuk kasing kosong, itu berarti Anda memiliki paling tidak a
LOAD_NAME
(yang harus mencari melalui namespace global dan__builtin__
modul ) diikuti oleh aCALL_FUNCTION
, yang harus mempertahankan bingkai saat ini:Anda dapat mengatur waktu pencarian nama secara terpisah dengan
timeit
:Perbedaan waktu mungkin ada tabrakan hash kamus. Kurangi waktu itu dari waktu untuk memanggil objek-objek itu, dan bandingkan hasilnya dengan waktu untuk menggunakan literal:
Jadi harus memanggil objek membutuhkan tambahan
1.00 - 0.31 - 0.30 == 0.39
detik per 10 juta panggilan.Anda dapat menghindari biaya pencarian global dengan menamai nama global sebagai penduduk lokal (menggunakan
timeit
pengaturan, semua yang Anda ikat ke nama adalah lokal):tetapi Anda tidak pernah bisa mengatasi
CALL_FUNCTION
biaya itu.sumber
list()
membutuhkan pencarian global dan panggilan fungsi tetapi[]
mengkompilasi ke satu instruksi. Lihat:sumber
Karena
list
adalah fungsi untuk mengkonversi katakanlah string ke objek daftar, sementara[]
digunakan untuk membuat daftar dari kelelawar. Coba ini (mungkin lebih masuk akal bagi Anda):Sementara
Memberi Anda daftar aktual yang berisi apa pun yang Anda masukkan ke dalamnya.
sumber
[]
lebih cepat daripadalist()
, bukan mengapa['wham bam']
lebih cepat daripadalist('wham bam')
.[]
/list()
persis sama dengan['wham']
/list('wham')
karena mereka memiliki perbedaan variabel1000/10
yang sama seperti100/1
pada matematika. Anda dapat secara teori mengambilwham bam
dan faktanya akan tetap sama, yanglist()
mencoba untuk mengubah sesuatu dengan memanggil nama fungsi sementara[]
akan langsung hanya mengubah variabel. Panggilan fungsi berbeda ya, ini hanya gambaran umum logis dari masalah seperti misalnya peta jaringan perusahaan juga logis dari solusi / masalah. Pilih bagaimanapun yang Anda inginkan.Jawaban di sini bagus, to the point dan sepenuhnya mencakup pertanyaan ini. Saya akan turun lebih jauh dari byte-code untuk mereka yang tertarik. Saya menggunakan repo CPython terbaru; versi yang lebih lama berperilaku serupa dalam hal ini tetapi sedikit perubahan mungkin terjadi.
Berikut adalah rincian eksekusi untuk masing-masing,
BUILD_LIST
untuk[]
danCALL_FUNCTION
untuklist()
.The
BUILD_LIST
instruksi:Anda hanya harus melihat kengeriannya:
Saya tahu, sangat berbelit-belit. Ini adalah betapa sederhananya:
PyList_New
(ini terutama mengalokasikan memori untuk objek daftar baru),oparg
menandakan jumlah argumen pada stack. Langsung ke intinya.if (list==NULL)
.PyList_SET_ITEM
(makro).Tidak heran itu cepat! Dibuat khusus untuk membuat daftar baru, tidak ada yang lain :-)
The
CALL_FUNCTION
instruksi:Inilah hal pertama yang Anda lihat ketika Anda mengintip penanganan kode
CALL_FUNCTION
:Terlihat tidak berbahaya, kan? Yah, tidak, sayangnya tidak,
call_function
bukan orang yang langsung akan memanggil fungsi segera, itu tidak bisa. Sebagai gantinya, ia mengambil objek dari stack, meraih semua argumen stack dan kemudian beralih berdasarkan pada jenis objek; apakah itu:PyCFunction_Type
? Tidak, itu adalahlist
,list
tidak dari jenisPyCFunction
PyMethodType
? Tidak, lihat sebelumnya.PyFunctionType
? Tidak, lihat sebelumnya.Kami memanggil
list
tipe, argumen yang diteruskancall_function
adalahPyList_Type
. CPython sekarang harus memanggil fungsi generik untuk menangani objek yang bisa dipanggil bernama_PyObject_FastCallKeywords
, yay lebih banyak panggilan fungsi.Fungsi ini lagi membuat beberapa pemeriksaan untuk jenis fungsi tertentu (yang saya tidak mengerti mengapa) dan kemudian, setelah membuat dikt untuk kwargs jika diperlukan , melanjutkan panggilan
_PyObject_FastCallDict
._PyObject_FastCallDict
akhirnya membawa kita ke suatu tempat! Setelah melakukan bahkan lebih cek itu meraihtp_call
slot yang daritype
daritype
kita sudah berlalu dalam, yaitu, meraihtype.tp_call
. Itu kemudian mulai membuat tuple dari argumen yang diteruskan_PyStack_AsTuple
dan, akhirnya, panggilan akhirnya dapat dibuat !tp_call
, yang cocoktype.__call__
mengambil alih dan akhirnya membuat objek daftar. Itu panggilan daftar__new__
yang sesuai denganPyType_GenericNew
dan mengalokasikan memori untuk itu denganPyType_GenericAlloc
: Ini sebenarnya bagian di mana ia mengejar ketinggalanPyList_New
, akhirnya . Semua yang sebelumnya diperlukan untuk menangani objek secara umum.Pada akhirnya,
type_call
panggilanlist.__init__
dan inisialisasi daftar dengan argumen yang tersedia, kemudian kami kembali seperti kami datang. :-)Akhirnya, ingat
LOAD_NAME
, itu cowok lain yang berkontribusi di sini.Sangat mudah untuk melihat bahwa, ketika berhadapan dengan input kami, Python umumnya harus melompat melalui lingkaran untuk benar-benar mengetahui
C
fungsi yang tepat untuk melakukan pekerjaan itu. Itu tidak memiliki ketepatan untuk segera menyebutnya karena itu dinamis, seseorang mungkin menutupilist
( dan anak laki-laki lakukan banyak orang ) dan jalan lain harus diambil.Di sinilah
list()
kehilangan banyak: Python mengeksplorasi perlu lakukan untuk mencari tahu apa yang harus dilakukan.Sintaks literal, di sisi lain, berarti tepat satu hal; itu tidak dapat diubah dan selalu berperilaku dengan cara yang ditentukan sebelumnya.
Catatan Kaki: Semua nama fungsi dapat berubah dari satu rilis ke rilis lainnya. Intinya masih berdiri dan kemungkinan besar akan berdiri di versi masa depan, itu adalah pencarian dinamis yang memperlambat segalanya.
sumber
Alasan terbesarnya adalah bahwa Python memperlakukan
list()
seperti fungsi yang ditentukan pengguna, yang berarti Anda dapat mencegatnya dengan mengalihkan sesuatu yang lain kelist
dan melakukan sesuatu yang berbeda (seperti menggunakan daftar subklas Anda sendiri atau mungkin deque).Segera membuat contoh baru dari daftar builtin dengan
[]
.Penjelasan saya berusaha memberi Anda intuisi untuk ini.
Penjelasan
[]
umumnya dikenal sebagai sintaksis literal.Dalam tata bahasa, ini disebut sebagai "tampilan daftar". Dari dokumen :
Singkatnya, ini berarti bahwa objek tipe builtin
list
dibuat.Tidak ada yang bisa mengelak dari ini - yang berarti Python dapat melakukannya secepat mungkin.
Di sisi lain,
list()
dapat dicegat dari membuat builtinlist
menggunakan konstruktor daftar builtin.Misalnya, kami ingin daftar kami dibuat dengan berisik:
Kami kemudian dapat mencegat nama
list
pada lingkup global level modul, dan kemudian ketika kami membuatlist
, kami benar-benar membuat daftar subtipe kami:Demikian pula kita dapat menghapusnya dari namespace global
dan letakkan di namespace builtin:
Dan sekarang:
Dan perhatikan bahwa tampilan daftar membuat daftar tanpa syarat:
Kami mungkin hanya melakukan ini sementara, jadi mari kita batalkan perubahan kami - pertama-tama hapus
List
objek baru dari bawaan:Oh, tidak, kami kehilangan jejak aslinya.
Tidak perlu khawatir, kita masih bisa mendapatkan
list
- itu adalah jenis daftar literal:Begitu...
Seperti yang telah kita lihat - kita dapat menimpa
list
- tetapi kita tidak dapat mencegat penciptaan tipe literal. Ketika kita menggunakanlist
kita harus melakukan pencarian untuk melihat apakah ada sesuatu di sana.Maka kita harus memanggil panggilan apa pun yang kita cari. Dari tata bahasa:
Kita dapat melihat bahwa ia melakukan hal yang sama untuk nama apa pun, tidak hanya daftar:
Karena
[]
tidak ada panggilan fungsi di tingkat bytecode Python:Itu hanya langsung membangun daftar tanpa pencarian atau panggilan di tingkat bytecode.
Kesimpulan
Kami telah menunjukkan bahwa
list
dapat dicegat dengan kode pengguna menggunakan aturan pelingkupan, dan yanglist()
mencari callable dan kemudian menyebutnya.Sedangkan
[]
tampilan daftar, atau literal, dan dengan demikian menghindari pencarian nama dan panggilan fungsi.sumber
list
dan kompiler python tidak dapat memastikan apakah itu benar-benar akan mengembalikan daftar kosong.