Python’s som vs. NumPy’s numpy.sum

Wat zijn de verschillen in prestatie en gedrag tussen het gebruik van de native sum-functie van Python en de numpy.sumvan NumPy? sumwerkt op de arrays van NumPy en numpy.sumwerkt op Python-lijsten en ze geven allebei hetzelfde effectieve resultaat (geen randgevallen zoals overflow getest), maar verschillende typen.

>>> import numpy as np
>>> np_a = np.array(range(5))
>>> np_a
array([0, 1, 2, 3, 4])
>>> type(np_a)
<class 'numpy.ndarray')
>>> py_a = list(range(5))
>>> py_a
[0, 1, 2, 3, 4]
>>> type(py_a)
<class 'list'>
# The numerical answer (10) is the same for the following sums:
>>> type(np.sum(np_a))
<class 'numpy.int32'>
>>> type(sum(np_a))
<class 'numpy.int32'>
>>> type(np.sum(py_a))
<class 'numpy.int32'>
>>> type(sum(py_a))
<class 'int'>

Bewerken:ik denk dat mijn praktische vraag hier is of het gebruik van numpy.sumop een lijst met gehele Python-getallen sneller is dan het gebruik van Python’s eigen sum?

Bovendien, wat zijn de implicaties (inclusief prestaties) van het gebruik van een geheel getal in Python versus een scalaire numpy.int32? Is er voor a += 1bijvoorbeeld een gedrags- of prestatieverschil als het type aeen geheel getal van Python of een numpy.int32? Ik ben benieuwd of het sneller is om een ​​NumPy scalair datatype zoals numpy.int32te gebruiken voor een waarde die veel wordt opgeteld of afgetrokken in Python-code.

Ter verduidelijking, ik werk aan een bioinformatica-simulatie die gedeeltelijk bestaat uit het samenvouwen van multidimensionale numpy.ndarrays in enkele scalaire sommen die vervolgens extra worden verwerkt. Ik gebruik Python 3.2 en NumPy 1.6.

Bij voorbaat dank!


Antwoord 1, autoriteit 100%

Ik werd nieuwsgierig en heb het getimed. numpy.sumlijkt veel sneller voor numpy-arrays, maar veel langzamer voor lijsten.

import numpy as np
import timeit
x = range(1000)
# or 
#x = np.random.standard_normal(1000)
def pure_sum():
    return sum(x)
def numpy_sum():
    return np.sum(x)
n = 10000
t1 = timeit.timeit(pure_sum, number = n)
print 'Pure Python Sum:', t1
t2 = timeit.timeit(numpy_sum, number = n)
print 'Numpy Sum:', t2

Resultaat wanneer x = range(1000):

Pure Python Sum: 0.445913167735
Numpy Sum: 8.54926219673

Resultaat wanneer x = np.random.standard_normal(1000):

Pure Python Sum: 12.1442425643
Numpy Sum: 0.303303771848

Ik gebruik Python 2.7.2 en Numpy 1.6.1


Antwoord 2, autoriteit 73%

[…] mijn […] vraag hier is of het gebruik van numpy.sumop een lijst met gehele Python-getallen sneller is dan het gebruik van Python’s eigen sum?

Het antwoord op deze vraag is: Nee.

Pythons-som zal sneller zijn op lijsten, terwijl NumPys-som sneller zal zijn op arrays. Ik heb eigenlijk een benchmark gedaan om de timing te laten zien (Python 3.6, NumPy 1.14):

import random
import numpy as np
import matplotlib.pyplot as plt
from simple_benchmark import benchmark
%matplotlib notebook
def numpy_sum(it):
    return np.sum(it)
def python_sum(it):
    return sum(it)
def numpy_sum_method(arr):
    return arr.sum()
b_array = benchmark(
    [numpy_sum, numpy_sum_method, python_sum],
    arguments={2**i: np.random.randint(0, 10, 2**i) for i in range(2, 21)},
    argument_name='array size',
    function_aliases={numpy_sum: 'numpy.sum(<array>)', numpy_sum_method: '<array>.sum()', python_sum: "sum(<array>)"}
)
b_list = benchmark(
    [numpy_sum, python_sum],
    arguments={2**i: [random.randint(0, 10) for _ in range(2**i)] for i in range(2, 21)},
    argument_name='list size',
    function_aliases={numpy_sum: 'numpy.sum(<list>)', python_sum: "sum(<list>)"}
)

Met deze resultaten:

f, (ax1, ax2) = plt.subplots(1, 2, sharey=True)
b_array.plot(ax=ax1)
b_list.plot(ax=ax2)

Links: op een NumPy-array; Rechts: op een Python-lijst.
Merk op dat dit een log-log plot is omdat de benchmark een zeer breed scala aan waarden bestrijkt. Maar voor kwalitatieve resultaten: lager betekent beter.

Wat aantoont dat voor lijsten Pythons sumaltijd sneller is, terwijl np.sumof de sum-methode op de array sneller zal zijn (behalve voor zeer korte arrays waar Pythons sumsneller is).

Voor het geval je deze met elkaar wilt vergelijken, heb ik ook een plot gemaakt waarin ze allemaal zijn opgenomen:

f, ax = plt.subplots(1)
b_array.plot(ax=ax)
b_list.plot(ax=ax)
ax.grid(which='both')

Interessant is dat het punt waarop numpykan concurreren op arrays met Python en lijsten ruwweg rond de 200 elementen ligt! Merk op dat dit aantal van veel factoren kan afhangen, zoals de Python/NumPy-versie, … Neem het niet te letterlijk.

Wat niet is genoemd, is de reden voor dit verschil (ik bedoel het grote schaalverschil, niet het verschil voor korte lijsten/arrays waar de functies gewoon een andere constante overhead hebben). Ervan uitgaande dat CPython een Python-lijst een wrapper is rond een C (de taal C) array van verwijzingen naar Python-objecten (in dit geval Python-getallen). Deze gehele getallen kunnen worden gezien als wrappers rond een geheel getal C (eigenlijk niet correct omdat gehele getallen van Python willekeurig groot kunnen zijn, dus het kan niet gewoon ééngeheel getal C gebruiken, maar het komt dicht genoeg in de buurt).

Een lijst als [1, 2, 3]zou bijvoorbeeld (schematisch, ik heb een paar details weggelaten) als volgt worden opgeslagen:

Een NumPy-array is echter een wrapper rond een C-array die C-waarden bevat (in dit geval intof longafhankelijk van 32 of 64bit en afhankelijk van het besturingssysteem) .

Dus een NumPy-array zoals np.array([1, 2, 3])ziet er als volgt uit:

Het volgende dat u moet begrijpen, is hoe deze functies werken:

  • Pythons sumherhaalt de iterabele (in dit geval de lijst of array) en voegt alle elementen toe.
  • NumPys summethodeherhaalt de opgeslagen C-array en voegt deze C-waarden toe en wikkelt die waarde uiteindelijk in een Python-type (in dit geval numpy.int32(of numpy.int64) en retourneert het.
  • NumPys sumfunctieconverteert de invoer naar een array(tenminste als het nog geen array is) en gebruikt vervolgens de NumPy summethode.

Het is duidelijk dat het toevoegen van C-waarden uit een C-array veel sneller gaat dan het toevoegen van Python-objecten. Daarom kunnen de NumPy-functies kunnenveel sneller zijn (zie de tweede grafiek hierboven, de NumPy-functies op arrays verslaan de Python-som verreweg voor grote arrays).

Maar het converteren van een Python-lijst naar een NumPy-array gaat relatief langzaam en dan moet je nog de C-waarden toevoegen. Daarom zal voor lijstende Python sumsneller zijn.

De enige openstaande vraag is waarom Pythons sumop een arrayzo traag is (het is de langzaamste van alle vergeleken functies). En dat heeft eigenlijk te maken met het feit dat Pythons som simpelweg itereert over wat je ook doorgeeft. In het geval van een lijst krijgt het het opgeslagen Python-objectmaar in het geval van een 1D NumPy-array zijn er geen opgeslagen Python-objecten, alleen C-waarden, dus Python&NumPy moeten een Python-object maken (een numpy.int32of numpy.int64) voor elk element en dan hebben deze Python-objecten toe te voegen. Het maken van de wrapper voor de C-waarde maakt het erg traag.

Bovendien, wat zijn de implicaties (inclusief prestaties) van het gebruik van een Python-integer versus een scalaire numpy.int32? Is er voor een += 1 bijvoorbeeld een gedrags- of prestatieverschil als het type a een Python-geheel getal of een numpy.int32 is?

Ik heb wat tests gedaan en voor optellen en aftrekken van scalaire getallen moet je je zeker houden aan Python-getallen. Ook al kan er enige caching plaatsvinden, wat betekent dat de volgende tests mogelijk niet helemaal representatief zijn:

from itertools import repeat
python_integer = 1000
numpy_integer_32 = np.int32(1000)
numpy_integer_64 = np.int64(1000)
def repeatedly_add_one(val):
    for _ in repeat(None, 100000):
        _ = val + 1
%timeit repeatedly_add_one(python_integer)
3.7 ms ± 71.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit repeatedly_add_one(numpy_integer_32)
14.3 ms ± 162 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit repeatedly_add_one(numpy_integer_64)
18.5 ms ± 494 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
def repeatedly_sub_one(val):
    for _ in repeat(None, 100000):
        _ = val - 1
%timeit repeatedly_sub_one(python_integer)
3.75 ms ± 236 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit repeatedly_sub_one(numpy_integer_32)
15.7 ms ± 437 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit repeatedly_sub_one(numpy_integer_64)
19 ms ± 834 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Het is 3-6 keer sneller om scalaire bewerkingen uit te voeren met Python integers dan met NumPy scalaires. Ik heb niet gecontroleerd waarom dat het geval is, maar ik vermoed dat NumPy-scalars zelden worden gebruikt en waarschijnlijk niet zijn geoptimaliseerd voor prestaties.

Het verschil wordt een beetje kleiner als je rekenkundige bewerkingen uitvoert waarbij beide operanden numpy scalairen zijn:

def repeatedly_add_one(val):
    one = type(val)(1)  # create a 1 with the same type as the input
    for _ in repeat(None, 100000):
        _ = val + one
%timeit repeatedly_add_one(python_integer)
3.88 ms ± 273 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit repeatedly_add_one(numpy_integer_32)
6.12 ms ± 324 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit repeatedly_add_one(numpy_integer_64)
6.49 ms ± 265 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Dan is het maar 2 keer langzamer.


Voor het geval je je afvroeg waarom ik itertools.repeathier gebruikte terwijl ik in plaats daarvan gewoon for _ in range(...)had kunnen gebruiken. De reden is dat repeatsneller is en dus minder overhead per lus met zich meebrengt. Omdat ik alleen geïnteresseerd ben in de tijd van optellen/aftrekken, is het eigenlijk beter om de looping overhead niet te laten knoeien met de timings (althans niet zo veel).


Antwoord 3, autoriteit 10%

Merk op dat Python-som op multidimensionale numpy-arrays alleen een som langs de eerste as uitvoert:

sum(np.array([[[2,3,4],[4,5,6]],[[7,8,9],[10,11,12]]]))
Out[47]: 
array([[ 9, 11, 13],
       [14, 16, 18]])
np.sum(np.array([[[2,3,4],[4,5,6]],[[7,8,9],[10,11,12]]]), axis=0)
Out[48]: 
array([[ 9, 11, 13],
       [14, 16, 18]])
np.sum(np.array([[[2,3,4],[4,5,6]],[[7,8,9],[10,11,12]]]))
Out[49]: 81

Antwoord 4, autoriteit 7%

Numpy zou veel sneller moeten zijn, vooral wanneer uw gegevens al een numpy-array zijn.

Numpy-arrays zijn een dunne laag over een standaard C-array. Wanneer numpy sum dit herhaalt, voert het geen typecontrole uit en is het erg snel. De snelheid moet vergelijkbaar zijn met het uitvoeren van de bewerking met standaard C.

Ter vergelijking: met de som van python moet het eerst de numpy-array converteren naar een python-array en vervolgens over die array herhalen. Het moet wat typecontrole uitvoeren en zal over het algemeen langzamer zijn.

Het exacte bedrag dat python sum langzamer is dan numpy sum is niet goed gedefinieerd, aangezien de python sum een ​​enigszins geoptimaliseerde functie zal zijn in vergelijking met het schrijven van je eigen sum-functie in python.


Antwoord 5, autoriteit 2%

Dit is een uitbreiding op de antwoordpost hierboven van Akavall. Uit dat antwoord kun je zien dat np.sumsneller presteert voor np.array-objecten, terwijl sumsneller presteert voor listobjecten. Om daarop uit te breiden:

Bij het uitvoeren van np.sumvoor een np.arrayobject Vs.sumvoor een listobject, het lijkt erop dat ze nek aan nek optreden.

# I'm running IPython
In [1]: x = range(1000) # list object
In [2]: y = np.array(x) # np.array object
In [3]: %timeit sum(x)
100000 loops, best of 3: 14.1 µs per loop
In [4]: %timeit np.sum(y)
100000 loops, best of 3: 14.3 µs per loop

Hierboven is sumeen kleinbeetje sneller dan np.array, hoewel ik soms np.sumtimings ook 14.1 µste zijn. Maar meestal is het 14.3 µs.


Antwoord 6

als je sum() gebruikt, dan krijg je

a = np.arange(6).reshape(2, 3)
print(a)
print(sum(a))
print(sum(sum(a)))
print(np.sum(a))
>>>
[[0 1 2]
 [3 4 5]]
[3 5 7]
15
15

Other episodes