Python dizileri neden yavaş?


153

array.arrayDiziler kutudan çıkarılmış gibi göründüğü için listelerden daha hızlı olmasını bekliyordum .

Ancak, aşağıdaki sonucu elde:

In [1]: import array

In [2]: L = list(range(100000000))

In [3]: A = array.array('l', range(100000000))

In [4]: %timeit sum(L)
1 loop, best of 3: 667 ms per loop

In [5]: %timeit sum(A)
1 loop, best of 3: 1.41 s per loop

In [6]: %timeit sum(L)
1 loop, best of 3: 627 ms per loop

In [7]: %timeit sum(A)
1 loop, best of 3: 1.39 s per loop

Böyle bir farkın nedeni ne olabilir?


4
Numpy araçları dizinizi verimli bir şekilde kullanabilir:% timeit np.sum (A): 100 döngü, döngü başına en iyi 3: 8,87 ms
BM

6
arrayPaketi kullanmam gereken bir durumla hiç karşılaşmadım . Önemli miktarda matematik yapmak istiyorsanız, Numpy ışık hızında (yani C) çalışır ve genellikle böyle şeylerin naif uygulamalarından daha iyidir sum()).
Nick T

40
Yakın seçmenler: Bu görüş neden tam olarak dayanmaktadır? OP, ölçülebilir ve tekrarlanabilir bir fenomen hakkında spesifik, teknik bir soru soruyor gibi görünüyor.
Kevin

5
@NickT Oku Bir optimizasyon fıkrası . Çıkıyor arraybir karşı tamsayılar (ASCII temsil bayt) bir dizi dönüştürme oldukça hızlıdır strnesne. Guido'nun kendisi ancak diğer birçok çözümden sonra geldi ve performansa oldukça şaşırdı. Her neyse, burası yararlı olduğunu hatırladığım tek yer. numpydizilerle uğraşmak için çok daha iyidir, ancak 3. tarafa bağımlıdır.
Bakuriu

Yanıtlar:


221

Depolama "Kutusuz" dir, ama her zaman onunla bir şey yapmak için (normal Python nesne içerisine sıkıştırılmak) "kutu" için Python sahip bir öğe erişin. Örneğin sum(A), dizinin üzerinde yineleme yapar ve her tamsayıyı birer birer normal bir Python intnesnesine koyar . Bu zaman alır. Gözlerinde farklı sum(L), tüm boks liste oluşturulduğu anda yapıldı.

Sonuç olarak, bir dizi genellikle daha yavaştır, ancak önemli ölçüde daha az bellek gerektirir.


İşte Python 3'ün son sürümünün ilgili kodu, ancak Python ilk kez piyasaya sürüldüğünden beri aynı temel fikirler tüm CPython uygulamaları için geçerlidir.

Bir liste öğesine erişmek için kod:

PyObject *
PyList_GetItem(PyObject *op, Py_ssize_t i)
{
    /* error checking omitted */
    return ((PyListObject *)op) -> ob_item[i];
}

Çok az şey var: somelist[i]sadece ilistedeki 'th nesnesini döndürür (ve CPython'daki tüm Python nesneleri, başlangıç ​​segmenti a düzenine uyan bir yapıya işaretçilerdir struct PyObject).

Ve işte tür kodlu __getitem__bir uygulama :arrayl

static PyObject *
l_getitem(arrayobject *ap, Py_ssize_t i)
{
    return PyLong_FromLong(((long *)ap->ob_item)[i]);
}

Ham bellek, platform-doğal C longtamsayıların bir vektörü olarak ele alınır ; i'inci C longkadar okunur; ve daha sonra PyLong_FromLong(), C longPython longnesnesindeki yerel metni sarmak ("kutu") olarak adlandırılır ( Python 3'te Python 2'nin intve arasındaki ayrımını ortadan kaldırır ve longaslında tür olarak gösterilir int).

Bu boks bir Python intnesnesi için yeni bellek ayırmalı ve yerel C longbitleri bu nesneye püskürtmelidir . Orijinal örnek bağlamında, bu nesnenin ömrü çok kısadır ( sum()içeriği toplama eklemek için yeterince uzun ) ve sonra yeni intnesneyi ayırmak için daha fazla zaman gerekir .

Bu, hız farkının geldiği, her zaman geldiği ve her zaman CPython uygulamasından geleceği yerdir.


87

Tim Peters'in mükemmel cevabına eklemek için, diziler arabellek protokolünü uygularken listeler uygulamaz . Bu, bir C uzantısı (veya bir Cython modülü yazmak gibi ahlaki bir eşdeğer ) yazıyorsanız , bir dizinin öğelerine Python'un yapabileceğinden çok daha hızlı erişip çalışabileceğiniz anlamına gelir. Bu, muhtemelen bir büyüklük sırasının çok üzerinde, önemli hız geliştirmeleri sağlayacaktır. Ancak, bir takım olumsuz yönleri vardır:

  1. Artık Python yerine C yazma işindesiniz. Cython bunu iyileştirmenin bir yoludur, ancak diller arasındaki birçok temel farkı ortadan kaldırmaz; C anlambilimine aşina olmanız ve ne yaptığını anlamanız gerekir.
  2. PyPy'nin C API'si bir dereceye kadar çalışıyor , ancak çok hızlı değil. PyPy'yi hedefliyorsanız, normal listelerle basit bir kod yazmanız ve ardından JITter'ın sizin için optimize etmesine izin vermeniz gerekir.
  3. C uzantılarının derlenmesi gerektiğinden saf Python kodundan daha zordur. Derleme, mimariye ve işletim sistemine bağımlı olma eğilimindedir, bu nedenle hedef platformunuz için derlediğinizden emin olmanız gerekir.

Doğrudan C uzantılarına gitmek, kullanım durumunuza bağlı olarak sinek uçurmak için balyoz kullanıyor olabilir. Önce NumPy'yi araştırmalı ve yapmaya çalıştığınız her matematik için yeterince güçlü olup olmadığını görmelisiniz. Doğru kullanılırsa, doğal Python'dan çok daha hızlı olacaktır.


10

Tim Peters bunun neden yavaş olduğunu yanıtladı , ancak nasıl iyileştirileceğini görelim .

Örneğinize yapışmak sum(range(...))(burada belleğe sığması için örneğinizden daha küçük faktör 10):

import numpy
import array
L = list(range(10**7))
A = array.array('l', L)
N = numpy.array(L)

%timeit sum(L)
10 loops, best of 3: 101 ms per loop

%timeit sum(A)
1 loop, best of 3: 237 ms per loop

%timeit sum(N)
1 loop, best of 3: 743 ms per loop

Bu şekilde numpy'nin ek yükü olan kutu / kutudan çıkarılması gerekir. Hızlı yapmak için numpy c kodu içinde kalmak gerekir:

%timeit N.sum()
100 loops, best of 3: 6.27 ms per loop

Yani liste çözümünden numpy sürümüne bu çalışma zamanında bir faktör 16.

Bu veri yapılarının oluşturulmasının ne kadar sürdüğünü de kontrol edelim

%timeit list(range(10**7))
1 loop, best of 3: 283 ms per loop

%timeit array.array('l', range(10**7))
1 loop, best of 3: 884 ms per loop

%timeit numpy.array(range(10**7))
1 loop, best of 3: 1.49 s per loop

%timeit numpy.arange(10**7)
10 loops, best of 3: 21.7 ms per loop

Açık kazanan: Numpy

Ayrıca, veri yapısının oluşturulmasının, daha fazla olmasa da, toplamanın kadar zaman aldığını unutmayın. Bellek ayırma yavaş.

Bunların bellek kullanımı:

sys.getsizeof(L)
90000112
sys.getsizeof(A)
81940352
sys.getsizeof(N)
80000096

Yani bunlar sayı başına 8 baytlık ve çeşitli ek yükler gerektiriyor. Kullandığımız aralık için 32bit ints yeterlidir, bu nedenle biraz bellek güvence altına alabiliriz.

N=numpy.arange(10**7, dtype=numpy.int32)

sys.getsizeof(N)
40000096

%timeit N.sum()
100 loops, best of 3: 8.35 ms per loop

Ancak, 64bit ints eklemenin makinemdeki 32bit ints'tan daha hızlı olduğu ortaya çıkıyor, bu yüzden sadece bellek / bant genişliği ile sınırlıysanız buna değer.


-1

unutmayın, buna 100000000eşit 10^8değildir 10^7ve sonuçlarım aşağıdaki gibidir:

100000000 == 10**8

# my test results on a Linux virtual machine:
#<L = list(range(100000000))> Time: 0:00:03.263585
#<A = array.array('l', range(100000000))> Time: 0:00:16.728709
#<L = list(range(10**8))> Time: 0:00:03.119379
#<A = array.array('l', range(10**8))> Time: 0:00:18.042187
#<A = array.array('l', L)> Time: 0:00:07.524478
#<sum(L)> Time: 0:00:01.640671
#<np.sum(L)> Time: 0:00:20.762153
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.