Mengapa pengembalian awal lebih lambat dari yang lain?

179

Ini adalah pertanyaan lanjutan untuk jawaban yang saya berikan beberapa hari yang lalu . Sunting: tampaknya OP dari pertanyaan itu sudah menggunakan kode yang saya poskan kepadanya untuk menanyakan pertanyaan yang sama , tetapi saya tidak menyadarinya. Permintaan maaf. Jawaban yang diberikan berbeda!

Secara substansial saya mengamati bahwa:

>>> def without_else(param=False):
...     if param:
...         return 1
...     return 0
>>> def with_else(param=False):
...     if param:
...         return 1
...     else:
...         return 0
>>> from timeit import Timer as T
>>> T(lambda : without_else()).repeat()
[0.3011460304260254, 0.2866089344024658, 0.2871549129486084]
>>> T(lambda : with_else()).repeat()
[0.27536892890930176, 0.2693932056427002, 0.27011704444885254]
>>> T(lambda : without_else(True)).repeat()
[0.3383951187133789, 0.32756996154785156, 0.3279120922088623]
>>> T(lambda : with_else(True)).repeat()
[0.3305950164794922, 0.32186388969421387, 0.3209099769592285]

... atau dengan kata lain: memiliki elseklausa lebih cepat terlepas dari ifkondisi yang dipicu atau tidak.

Saya menganggap itu ada hubungannya dengan bytecode berbeda yang dihasilkan oleh keduanya, tetapi adakah yang bisa mengkonfirmasi / menjelaskan secara detail?

EDIT: Sepertinya tidak semua orang dapat mereproduksi timing saya, jadi saya pikir mungkin berguna untuk memberikan beberapa informasi tentang sistem saya. Saya menjalankan Ubuntu 11.10 64 bit dengan python default diinstal. pythonmenghasilkan informasi versi berikut:

Python 2.7.2+ (default, Oct  4 2011, 20:06:09) 
[GCC 4.6.1] on linux2

Berikut adalah hasil pembongkaran di Python 2.7:

>>> dis.dis(without_else)
  2           0 LOAD_FAST                0 (param)
              3 POP_JUMP_IF_FALSE       10

  3           6 LOAD_CONST               1 (1)
              9 RETURN_VALUE        

  4     >>   10 LOAD_CONST               2 (0)
             13 RETURN_VALUE        
>>> dis.dis(with_else)
  2           0 LOAD_FAST                0 (param)
              3 POP_JUMP_IF_FALSE       10

  3           6 LOAD_CONST               1 (1)
              9 RETURN_VALUE        

  5     >>   10 LOAD_CONST               2 (0)
             13 RETURN_VALUE        
             14 LOAD_CONST               0 (None)
             17 RETURN_VALUE        
Mac
sumber
1
ada pertanyaan yang identik pada SO saya tidak dapat menemukan sekarang. Mereka memeriksa bytecode yang dihasilkan dan ada satu langkah tambahan. Perbedaan yang diamati sangat tergantung pada tester (mesin, SO ..), kadang-kadang hanya menemukan perbedaan yang sangat kecil.
joaquin
3
Pada 3.x, keduanya menghasilkan bytecode yang identik, simpan untuk beberapa kode yang tidak dapat dijangkau ( LOAD_CONST(None); RETURN_VALUE- tetapi seperti yang disebutkan, itu tidak pernah tercapai) di akhir with_else. Saya sangat meragukan kode mati membuat fungsi lebih cepat. Bisakah seseorang memberikan dispada 2.7?
4
Saya tidak dapat mereproduksi ini. Berfungsi dengan elsedan Falsepaling lambat dari semuanya (152ns). Tercepat kedua adalah Truetanpa else(143ns) dan dua lainnya pada dasarnya sama (137ns dan 138ns). Saya tidak menggunakan parameter default dan mengukurnya dengan %timeitdi iPython.
rplnt
Saya tidak dapat mereproduksi timing-timing itu, terkadang with_else lebih cepat, terkadang ini adalah versi without_else, sepertinya mereka sangat mirip dengan saya ...
Cédric Julien
1
Menambahkan hasil pembongkaran. Saya menggunakan Ubuntu 11.10, 64-bit, stock Python 2.7 - konfigurasi yang sama dengan @mac. Saya juga setuju bahwa with_elseini lebih cepat terlihat.
Chris Morgan

Jawaban:

387

Ini dugaan murni, dan saya belum menemukan cara mudah untuk memeriksa apakah itu benar, tetapi saya punya teori untuk Anda.

Saya mencoba kode Anda dan mendapatkan hasil yang sama, without_else()berulang kali sedikit lebih lambat daripada with_else():

>>> T(lambda : without_else()).repeat()
[0.42015745017874906, 0.3188967452567226, 0.31984281521812363]
>>> T(lambda : with_else()).repeat()
[0.36009842032996175, 0.28962249392031936, 0.2927151355828528]
>>> T(lambda : without_else(True)).repeat()
[0.31709728471076915, 0.3172671387005721, 0.3285821242644147]
>>> T(lambda : with_else(True)).repeat()
[0.30939889008243426, 0.3035132258429485, 0.3046679117038593]

Mempertimbangkan bahwa bytecode identik, satu-satunya perbedaan adalah nama fungsi. Secara khusus, tes timing melakukan pencarian pada nama global. Coba ganti nama without_else()dan perbedaannya menghilang:

>>> def no_else(param=False):
    if param:
        return 1
    return 0

>>> T(lambda : no_else()).repeat()
[0.3359846013948413, 0.29025818923918223, 0.2921801513879245]
>>> T(lambda : no_else(True)).repeat()
[0.3810395594970828, 0.2969634408842694, 0.2960104566362247]

Dugaan saya adalah bahwa without_elsememiliki tabrakan hash dengan sesuatu yang lain globals()sehingga pencarian nama global sedikit lebih lambat.

Sunting : Kamus dengan 7 atau 8 kunci mungkin memiliki 32 slot, jadi atas dasar itu without_elsememiliki tabrakan hash dengan __builtins__:

>>> [(k, hash(k) % 32) for k in globals().keys() ]
[('__builtins__', 8), ('with_else', 9), ('__package__', 15), ('without_else', 8), ('T', 21), ('__name__', 25), ('no_else', 28), ('__doc__', 29)]

Untuk memperjelas cara hashing bekerja:

__builtins__ hashes ke -1196389688 yang mengurangi modulo ukuran tabel (32) berarti disimpan dalam slot # 8 tabel.

without_elsehash ke 505688136 yang mengurangi modulo 32 adalah 8 sehingga ada tabrakan. Untuk menyelesaikan perhitungan Python ini:

Dimulai dengan:

j = hash % 32
perturb = hash

Ulangi ini sampai kami menemukan slot gratis:

j = (5*j) + 1 + perturb;
perturb >>= 5;
use j % 2**i as the next table index;

yang memberikannya 17 untuk digunakan sebagai indeks berikutnya. Untungnya itu gratis sehingga loop hanya berulang sekali. Ukuran tabel hash adalah kekuatan 2, begitu 2**ijuga ukuran tabel hash, iadalah jumlah bit yang digunakan dari nilai hash j.

Setiap probe ke dalam tabel dapat menemukan salah satunya:

  • Slot kosong, dalam hal ini probing berhenti dan kami tahu nilainya tidak ada dalam tabel.

  • Slot tidak digunakan tetapi digunakan di masa lalu dalam hal ini kita coba nilai selanjutnya yang dihitung seperti di atas.

  • Slot penuh tetapi nilai hash penuh yang disimpan dalam tabel tidak sama dengan hash kunci yang kita cari (itulah yang terjadi dalam kasus __builtins__vs without_else).

  • Slot penuh dan memiliki nilai hash persis yang kita inginkan, maka Python memeriksa untuk melihat apakah kunci dan objek yang kita cari adalah objek yang sama (yang dalam hal ini mereka akan karena string pendek yang bisa menjadi pengidentifikasi diinternir sehingga pengidentifikasi identik menggunakan string yang sama persis).

  • Akhirnya ketika slot penuh, hash cocok persis, tetapi kunci bukan objek yang identik, maka dan hanya saat itu Python akan mencoba membandingkannya untuk kesetaraan. Ini relatif lambat, tetapi dalam kasus pencarian nama seharusnya tidak benar-benar terjadi.

Duncan
sumber
9
@ Chris, tidak ada panjang tali tidak harus signifikan. Pertama kali Anda hash string, akan butuh waktu sebanding dengan panjang string tetapi kemudian hash yang dihitung di-cache dalam objek string sehingga hash berikutnya adalah O (1).
Duncan
1
Ah ok, saya tidak menyadari caching, tapi itu masuk akal
Chris Eberle
9
Menarik! Boleh aku memanggilmu Sherlock? ;) Bagaimanapun saya harap saya tidak akan lupa untuk memberi Anda beberapa poin tambahan dengan hadiah segera setelah pertanyaan memenuhi syarat.
Voo
4
@ Mac, tidak cukup. Saya akan menambahkan sedikit tentang resolusi hash (akan memerasnya ke dalam komentar tetapi lebih menarik daripada yang saya pikir).
Duncan
6
@Duncan - Terima kasih banyak telah meluangkan waktu untuk menggambarkan proses hash. Jawaban kedudukan tertinggi! :)
mac