Menyalin range(10**6)
daftar yang diacak sepuluh kali membutuhkan waktu sekitar 0,18 detik: (ini adalah lima proses)
0.175597017661
0.173731403198
0.178601711594
0.180330912952
0.180811964451
Menyalin daftar yang tidak diacak sepuluh kali membutuhkan waktu sekitar 0,05 detik:
0.058402235973
0.0505464636856
0.0509734306934
0.0526022752744
0.0513324916184
Inilah kode pengujian saya:
from timeit import timeit
import random
a = range(10**6)
random.shuffle(a) # Remove this for the second test.
a = list(a) # Just an attempt to "normalize" the list.
for _ in range(5):
print timeit(lambda: list(a), number=10)
Saya juga mencoba menyalin dengan a[:]
, hasilnya serupa (yaitu, perbedaan kecepatan yang besar)
Mengapa perbedaan kecepatannya besar? Saya tahu dan memahami perbedaan kecepatan dalam array terkenal Mengapa lebih cepat untuk memproses array yang diurutkan daripada array yang tidak disortir? Misalnya, tetapi di sini pemrosesan saya tidak memiliki keputusan. Itu hanya menyalin referensi di dalam daftar secara membabi buta, bukan?
Saya menggunakan Python 2.7.12 di Windows 10.
Sunting: Mencoba Python 3.5.2 juga sekarang, hasilnya hampir sama (diacak secara konsisten sekitar 0,17 detik, diacak secara konsisten sekitar 0,05 detik). Berikut kode untuk itu:
a = list(range(10**6))
random.shuffle(a)
a = list(a)
for _ in range(5):
print(timeit(lambda: list(a), number=10))
sumber
0.25
di setiap iterasi dari setiap tes. Jadi di platform saya, urutan itu penting.Jawaban:
Hal yang menarik adalah bahwa itu tergantung pada urutan bilangan bulat pertama kali dibuat. Misalnya, alih-alih
shuffle
membuat urutan acak denganrandom.randint
:from timeit import timeit import random a = [random.randint(0, 10**6) for _ in range(10**6)] for _ in range(5): print(timeit(lambda: list(a), number=10))
Ini secepat menyalin
list(range(10**6))
(contoh pertama dan cepat) Anda.Namun ketika Anda mengocok - maka bilangan bulat Anda tidak dalam urutan saat pertama kali dibuat lagi, itulah yang membuatnya lambat.
Selingan singkat:
Py_INCREF
dalamlist_slice
), jadi Python benar-benar perlu pergi ke tempat objek itu berada. Itu tidak bisa hanya menyalin referensi.Jadi, ketika Anda menyalin daftar Anda, Anda mendapatkan setiap item dari daftar itu dan meletakkannya "sebagaimana adanya" di daftar baru. Saat item Anda berikutnya dibuat tidak lama setelah item saat ini, ada kemungkinan besar (tidak ada jaminan!) Bahwa item tersebut disimpan di sebelahnya di heap.
Mari kita asumsikan bahwa setiap kali komputer Anda memuat item dalam cache, ia juga memuat
x
item berikutnya di memori (lokalitas cache). Kemudian komputer Anda dapat melakukan penambahan jumlah referensi untukx+1
item pada cache yang sama!Dengan urutan yang diacak, ini masih memuat item berikutnya dalam memori tetapi ini bukan yang berikutnya dalam daftar. Jadi ia tidak dapat melakukan kenaikan jumlah referensi tanpa "benar-benar" mencari item berikutnya.
TL; DR: Kecepatan sebenarnya bergantung pada apa yang terjadi sebelum penyalinan: dalam urutan apa item ini dibuat dan dalam urutan apa item ini ada dalam daftar.
Anda dapat memverifikasi ini dengan melihat
id
:a = list(range(10**6, 10**6+100)) for item in a: print(id(item))
Hanya untuk menampilkan kutipan singkat:
1496489995888 1496489995920 # +32 1496489995952 # +32 1496489995984 # +32 1496489996016 # +32 1496489996048 # +32 1496489996080 # +32 1496489996112 1496489996144 1496489996176 1496489996208 1496489996240 1496507297840 1496507297872 1496507297904 1496507297936 1496507297968 1496507298000 1496507298032 1496507298064 1496507298096 1496507298128 1496507298160 1496507298192
Jadi objek ini benar-benar "bersebelahan di heap". Dengan
shuffle
mereka tidak:import random a = list(range(10**6, 100+10**6)) random.shuffle(a) last = None for item in a: if last is not None: print('diff', id(item) - id(last)) last = item
Yang menunjukkan ini tidak benar-benar bersebelahan dalam memori:
diff 736 diff -64 diff -17291008 diff -128 diff 288 diff -224 diff 17292032 diff -1312 diff 1088 diff -17292384 diff 17291072 diff 608 diff -17290848 diff 17289856 diff 928 diff -672 diff 864 diff -17290816 diff -128 diff -96 diff 17291552 diff -192 diff 96 diff -17291904 diff 17291680 diff -1152 diff 896 diff -17290528 diff 17290816 diff -992 diff 448
Catatan penting:
Saya sendiri belum memikirkan ini. Sebagian besar informasi dapat ditemukan di postingan blog Ricky Stewart .
Jawaban ini didasarkan pada implementasi CPython "resmi" dari Python. Detail dalam implementasi lain (Jython, PyPy, IronPython, ...) mungkin berbeda. Terima kasih @ JörgWMittag telah menunjukkan hal ini .
sumber
list_slice
dan pada baris 453 Anda dapat melihatPy_INCREF(v);
panggilan yang perlu mengakses objek yang dialokasikan heap.a = [0] * 10**7
(naik dari 10 ** 6 karena itu terlalu tidak stabil), yang bahkan lebih cepat daripada menggunakana = range(10**7)
(dengan faktor sekitar 1,25). Jelas karena itu lebih baik untuk caching.[0,1,2,3]*((10**6) // 4)
secepata = [0] * 10**6
. Namun dengan bilangan bulat dari 0-255 ada fakta lain yang masuk: Ini disimpan sehingga dengan ini urutan pembuatan (di dalam skrip Anda) tidak penting lagi - karena mereka dibuat ketika Anda memulai python.Saat Anda mengacak item daftar, item tersebut memiliki lokalitas referensi yang lebih buruk, yang menyebabkan kinerja cache lebih buruk.
Anda mungkin berpikir bahwa menyalin daftar hanya menyalin referensi, bukan objek, jadi lokasinya di heap seharusnya tidak menjadi masalah. Namun, menyalin masih melibatkan mengakses setiap objek untuk memodifikasi refcount.
sumber
Seperti yang dijelaskan oleh orang lain, itu tidak hanya menyalin referensi tetapi juga meningkatkan jumlah referensi di dalam benda-benda dan dengan demikian benda yang diakses dan cache berperan.
Di sini saya hanya ingin menambahkan lebih banyak eksperimen. Tidak begitu banyak tentang shuffled vs unshuffled (di mana mengakses satu elemen mungkin melewatkan cache tetapi memasukkan elemen-elemen berikut ke dalam cache sehingga mereka terkena). Tetapi tentang elemen berulang, di mana akses nanti dari elemen yang sama mungkin mengenai cache karena elemen tersebut masih dalam cache.
Menguji rentang normal:
>>> from timeit import timeit >>> a = range(10**7) >>> [timeit(lambda: list(a), number=100) for _ in range(3)] [5.1915339142808925, 5.1436351868889645, 5.18055115701749]
Daftar dengan ukuran yang sama tetapi dengan hanya satu elemen yang diulang terus-menerus lebih cepat karena selalu menyentuh cache:
>>> a = [0] * 10**7 >>> [timeit(lambda: list(a), number=100) for _ in range(3)] [4.125743135926939, 4.128927210087596, 4.0941229388550795]
Dan sepertinya tidak masalah berapa jumlahnya:
>>> a = [1234567] * 10**7 >>> [timeit(lambda: list(a), number=100) for _ in range(3)] [4.124106479141709, 4.156590225249886, 4.219242600790949]
Menariknya, itu menjadi lebih cepat ketika saya malah mengulangi dua atau empat elemen yang sama:
>>> a = [0, 1] * (10**7 / 2) >>> [timeit(lambda: list(a), number=100) for _ in range(3)] [3.130586101607932, 3.1001001764957294, 3.1318465707127814] >>> a = [0, 1, 2, 3] * (10**7 / 4) >>> [timeit(lambda: list(a), number=100) for _ in range(3)] [3.096105435911994, 3.127148431279352, 3.132872673690855]
Saya kira ada sesuatu yang tidak suka penghitung tunggal yang sama meningkat sepanjang waktu. Mungkin beberapa jalur pipa terhenti karena setiap kenaikan harus menunggu hasil dari peningkatan sebelumnya, tetapi ini adalah tebakan liar.
Bagaimanapun, coba ini untuk jumlah elemen berulang yang lebih besar:
from timeit import timeit for e in range(26): n = 2**e a = range(n) * (2**25 / n) times = [timeit(lambda: list(a), number=20) for _ in range(3)] print '%8d ' % n, ' '.join('%.3f' % t for t in times), ' => ', sum(times) / 3
Outputnya (kolom pertama adalah jumlah elemen yang berbeda, untuk setiap saya menguji tiga kali dan kemudian mengambil rata-rata):
1 2.871 2.828 2.835 => 2.84446732686 2 2.144 2.097 2.157 => 2.13275338734 4 2.129 2.297 2.247 => 2.22436720645 8 2.151 2.174 2.170 => 2.16477771575 16 2.164 2.159 2.167 => 2.16328197911 32 2.102 2.117 2.154 => 2.12437970598 64 2.145 2.133 2.126 => 2.13462250728 128 2.135 2.122 2.137 => 2.13145065221 256 2.136 2.124 2.140 => 2.13336283943 512 2.140 2.188 2.179 => 2.1688431668 1024 2.162 2.158 2.167 => 2.16208440826 2048 2.207 2.176 2.213 => 2.19829998424 4096 2.180 2.196 2.202 => 2.19291917834 8192 2.173 2.215 2.188 => 2.19207065277 16384 2.258 2.232 2.249 => 2.24609975704 32768 2.262 2.251 2.274 => 2.26239771771 65536 2.298 2.264 2.246 => 2.26917420394 131072 2.285 2.266 2.313 => 2.28767871168 262144 2.351 2.333 2.366 => 2.35030805124 524288 2.932 2.816 2.834 => 2.86047313113 1048576 3.312 3.343 3.326 => 3.32721167007 2097152 3.461 3.451 3.547 => 3.48622758473 4194304 3.479 3.503 3.547 => 3.50964316455 8388608 3.733 3.496 3.532 => 3.58716466865 16777216 3.583 3.522 3.569 => 3.55790996695 33554432 3.550 3.556 3.512 => 3.53952594744
Jadi dari sekitar 2,8 detik untuk satu elemen (berulang) turun menjadi sekitar 2,2 detik untuk 2, 4, 8, 16, ... elemen yang berbeda dan tetap di sekitar 2,2 detik hingga ratusan ribu. Saya pikir ini menggunakan cache L2 saya (4 × 256 KB, saya memiliki i7-6700 ).
Kemudian selama beberapa langkah, waktunya naik menjadi 3,5 detik. Saya rasa ini menggunakan campuran cache L2 saya dan cache L3 saya (8 MB) hingga "habis" juga.
Pada akhirnya itu tetap sekitar 3,5 detik, saya kira karena cache saya tidak lagi membantu dengan elemen yang berulang.
sumber
Sebelum pengacakan, saat dialokasikan di heap, objek indeks yang berdekatan berdekatan dalam memori, dan tingkat ketepatan memori tinggi saat diakses; setelah pengocokan, objek dari indeks yang berdekatan dari daftar baru tidak ada dalam memori. Berdekatan, hit rate sangat buruk.
sumber