A.insert (0,0) neden bir [0: 0] = [0] 'dan daha yavaş?


61

Bir listenin insertişlevini kullanmak, dilim atamasını kullanarak aynı efekti elde etmekten çok daha yavaştır:

> 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

( a=[]Yalnızca kurulum olduğunu unutmayın , bu nedenle aboş başlar ancak 100.000 öğeye kadar büyür.)

İlk başta belki bu özellik arama veya fonksiyon çağrısı ya da öylesine düşündüm, ama sonuna yakın eklemek bu ihmal edilebilir gösterir:

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

Muhtemelen daha basit özel "tek eleman ekle" fonksiyonu neden bu kadar yavaş?

Ayrıca repl.it de çoğaltabilir :

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)

Windows 10 64 bit'te Python 3.8.1 32 bit kullanıyorum.
repl.it Linux 64-bit üzerinde Python 3.8.1 64-bit kullanır.


Not a=[]; a[0:0]=[0]aynı a=[]; a[100:200]=[0]
ilginç

Bunu sadece boş bir listeyle test etmenizin bir nedeni var mı?
MisterMiyagi

@MisterMiyagi Şey, bir şeyle başlamak zorundayım . Yalnızca ilk eklemeden önce boş olduğunu ve karşılaştırma sırasında 100.000 öğeye kadar büyüdüğünü unutmayın.
Öbek Taşması

@ smac89 a=[1,2,3];a[100:200]=[4]ekliyordur 4liste sonuna ailginç.
Ch3steR

1
@ smac89 Bu doğru olsa da, gerçekten soru ile ilgisi yoktur ve korkarım ki, kıyasladığımı a=[]; a[0:0]=[0]veya bununla a[0:0]=[0]aynı şeyi yaptığımı düşünmesi için birini yanıltabilir a[100:200]=[0].
Heap Overflow

Yanıtlar:


57

Ben onlar kullanmayı unuttum sadece muhtemelen düşünüyorum memmoveiçinde list.insert. Öğeleri kaydırmak için kullanılan kodlara bakarsanız, list.insertbunun sadece manuel bir döngü olduğunu görebilirsiniz:

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

ederken list.__setitem__dilim atama yolu üzerinde kullanımlarmemmove :

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

memmove tipik olarak SSE / AVX talimatlarından yararlanmak gibi birçok optimizasyona sahiptir.


5
Teşekkürler. Buna referans vererek bir sorun oluşturuldu .
Öbek Taşması

7
Yorumlayıcı -O3otomatik vektörleştirme etkinken oluşturulmuşsa, bu manuel döngü verimli bir şekilde derlenebilir. Ancak derleyici, döngüyü bir bellek olarak tanımadığı ve gerçek bir çağrı olarak memmovederlemediği sürece, yalnızca derleme zamanında etkinleştirilen komut kümesi uzantılarından yararlanabilir. (Eğer kendi ile inşa ediyorsanız -march=native, temel ile inşa edilen dağıtım ikili için çok fazla değil). PGO ( -fprofile-generate/ run / ...-use) kullanmadığınız sürece GCC varsayılan olarak döngülerin kilidini açmaz
Peter Cordes

@PeterCordes Derleyicinin gerçek bir memmoveçağrıya derlemesi durumunda , yürütme sırasında mevcut olan tüm uzantılardan yararlanabileceğini doğru anlıyor musunuz ?
Yığın Taşması

1
@HeapOverflow: Evet. Örneğin GNU / Linux'ta, glibc, kaydedilen CPU algılama sonuçlarına dayanarak bu makine için en iyi elle yazılmış asm sürümünü seçen bir işlevle dinamik bağlayıcı sembol çözünürlüğünü aşırı yükler. (örneğin x86'da bir glibc init işlevi kullanılır cpuid). Diğer bazı mem / str işlevleri için aynıdır. Bu nedenle dağıtımlar yalnızca -O2her yerde çalışma ikili dosyaları oluşturmak için derlenebilir , ancak en azından memcpy / memmove komut başına 32 bayt yükleme / depolama gibi bir kaydedilmemiş AVX döngü kullanmalıdır. (Ya da bunun iyi bir fikir olduğu birkaç CPU'da AVX512 bile; sanırım sadece Xeon Phi.)
Peter Cordes

1
@HeapOverflow: Hayır, memmovepaylaşılan kütüphane libc.so'da birkaç sürüm var. Her işlev için, gönderme bir kez, sembol çözünürlüğü sırasında (erken bağlama veya geleneksel tembel bağlama ile ilk çağrıda) gerçekleşir. Dediğim gibi, fonksiyonun kendisini sararak değil, dinamik bağlamanın nasıl gerçekleştiğini aşırı yükler / kancalar. (özellikle GCC'nin ifunc mekanizması aracılığıyla: code.woboq.org/userspace/glibc/sysdeps/x86_64/multiarch/… ). İlgili: memset için modern CPU'larda her zamanki seçenek __memset_avx2_unaligned_erms bu Q&A'ya bakın
Peter Cordes
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.