Mengapa a.insert (0,0) jauh lebih lambat daripada [0: 0] = [0]?

61

Menggunakan insertfungsi daftar jauh lebih lambat daripada mencapai efek yang sama menggunakan tugas slice:

> python -m timeit -n 100000 -s "a=[]" "a.insert(0,0)"
100000 loops, best of 5: 19.2 usec per loop

> python -m timeit -n 100000 -s "a=[]" "a[0:0]=[0]"
100000 loops, best of 5: 6.78 usec per loop

(Perhatikan bahwa a=[]ini hanya pengaturan, jadi amulai kosong tetapi kemudian tumbuh hingga 100.000 elemen.)

Pada awalnya saya pikir mungkin itu atribut lookup atau function call overhead atau lebih, tetapi menyisipkan di dekat akhir menunjukkan bahwa itu dapat diabaikan:

> python -m timeit -n 100000 -s "a=[]" "a.insert(-1,0)"
100000 loops, best of 5: 79.1 nsec per loop

Mengapa fungsi "sisipkan satu elemen" khusus yang mungkin lebih sederhana sangat lambat?

Saya juga dapat mereproduksi di repl.it :

from timeit import repeat

for _ in range(3):
  for stmt in 'a.insert(0,0)', 'a[0:0]=[0]', 'a.insert(-1,0)':
    t = min(repeat(stmt, 'a=[]', number=10**5))
    print('%.6f' % t, stmt)
  print()

# Example output:
#
# 4.803514 a.insert(0,0)
# 1.807832 a[0:0]=[0]
# 0.012533 a.insert(-1,0)
#
# 4.967313 a.insert(0,0)
# 1.821665 a[0:0]=[0]
# 0.012738 a.insert(-1,0)
#
# 5.694100 a.insert(0,0)
# 1.899940 a[0:0]=[0]
# 0.012664 a.insert(-1,0)

Saya menggunakan Python 3.8.1 32-bit pada Windows 10 64-bit.
repl.it menggunakan Python 3.8.1 64-bit di Linux 64-bit.

Heap Overflow
sumber
Menarik untuk dicatat yang a=[]; a[0:0]=[0]melakukan hal yang samaa=[]; a[100:200]=[0]
smac89
Apakah ada alasan mengapa Anda menguji ini hanya dengan daftar kosong?
MisterMiyagi
@MisterMiyagi Baiklah, saya harus mulai dengan sesuatu . Perhatikan bahwa kosong hanya sebelum penyisipan pertama dan tumbuh hingga 100.000 elemen selama tolok ukur.
Heap Overflow
@ smac89 a=[1,2,3];a[100:200]=[4]menambahkan 4ke akhir daftar yang amenarik.
Ch3steR
1
@ smac89 Walaupun itu benar, itu tidak benar-benar ada hubungannya dengan pertanyaan dan saya khawatir itu mungkin menyesatkan seseorang untuk berpikir bahwa saya melakukan benchmarking a=[]; a[0:0]=[0]atau yang a[0:0]=[0]melakukan hal yang sama a[100:200]=[0]...
Heap Overflow

Jawaban:

57

Saya pikir itu mungkin hanya bahwa mereka lupa untuk menggunakan memmovedi list.insert. Jika Anda melihat kode yang list.insert digunakan untuk menggeser elemen, Anda bisa melihatnya hanya loop manual:

for (i = n; --i >= where; )
    items[i+1] = items[i];

saat list.__setitem__di jalur penugasan irisan menggunakanmemmove :

memmove(&item[ihigh+d], &item[ihigh],
    (k - ihigh)*sizeof(PyObject *));

memmove biasanya memiliki banyak optimasi yang dimasukkan ke dalamnya, seperti memanfaatkan instruksi SSE / AVX.

user2357112 mendukung Monica
sumber
5
Terima kasih. Membuat masalah referensi ini.
Heap Overflow
7
Jika penerjemah dibangun dengan -O3auto-vektorisasi diaktifkan, loop manual yang mungkin dikompilasi secara efisien. Tetapi kecuali jika kompiler mengenali loop sebagai memmove dan mengkompilasinya menjadi panggilan aktual memmove, ia hanya dapat memanfaatkan ekstensi set instruksi yang diaktifkan pada waktu kompilasi. (Baik jika Anda sedang membangun dengan Anda sendiri -march=native, tidak terlalu banyak untuk biner distro yang dibangun dengan baseline). Dan GCC tidak akan membuka gulungan secara default kecuali jika Anda menggunakan PGO ( -fprofile-generate/ run / ...-use)
Peter Cordes
@PeterCordes Apakah saya mengerti Anda dengan benar bahwa jika kompiler tidak mengkompilasinya menjadi memmovepanggilan aktual , yang kemudian dapat mengambil keuntungan dari semua ekstensi yang ada pada waktu eksekusi?
Heap Overflow
1
@HeapOverflow: Ya. Pada GNU / Linux misalnya, glibc membebani resolusi simbol linker dinamis dengan fungsi yang memilih versi memmove tulisan tangan terbaik untuk mesin ini berdasarkan pada hasil deteksi CPU yang tersimpan. (misalnya pada x86, fungsi init glibc menggunakan cpuid). Sama untuk beberapa fungsi mem / str lainnya. Jadi distro dapat dikompilasi dengan hanya -O2untuk membuat binari run-dimanapun, tetapi setidaknya memcpy / memmove menggunakan AVX loop memuat / menyimpan 32 byte per instruksi. (Atau bahkan AVX512 pada beberapa CPU di mana itu ide yang bagus; Saya pikir hanya Xeon Phi.)
Peter Cordes
1
@HeapOverflow: Tidak, beberapa memmoveversi duduk di sana di libc.so, pustaka bersama. Untuk setiap fungsi, pengiriman terjadi satu kali, selama resolusi simbol (penjilidan awal atau pada panggilan pertama dengan lazy binding tradisional). Seperti yang saya katakan, itu hanya overloads / kait bagaimana hubungan dinamis terjadi, bukan dengan membungkus fungsi itu sendiri. (khusus melalui mekanisme ifunc GCC: code.woboq.org/userspace/glibc/sysdeps/x86_64/multiarch/… ). Terkait: untuk memset pilihan yang biasa pada CPU modern adalah __memset_avx2_unaligned_erms lihat T&J ini
Peter Cordes