我看到了一個關于 python 中回圈速度的視頻,其中解釋說這樣做sum(range(N))比手動回圈range并將變數加在一起要快得多,因為前者由于使用了內置函式而在 C 中運行,而在后者中求和是在(慢)python 中完成的。我很好奇加入numpy混合物時會發生什么。如我所料np.sum(np.arange(N))的是最快的,但sum(np.arange(N))和np.sum(range(N))甚至慢于做天真的for回圈。
為什么是這樣?
這是我用來測驗的腳本,一些關于我知道的減速原因的評論(主要來自視頻)以及我在我的機器上得到的結果(python 3.8.10,numpy 1.19.5):
更新腳本:
import numpy as np
from timeit import timeit
N = 10_000_000
repetition = 10
def sum0(N = N):
s = 0
i = 0
while i < N: # condition is checked in python
s = i
i = 1 # both additions are done in python
return s
def sum1(N = N):
s = 0
for i in range(N): # increment in C
s = i # addition in python
return s
def sum2(N = N):
return sum(range(N)) # everything in C
def sum3(N = N):
return sum(list(range(N)))
def sum4(N = N):
return np.sum(range(N)) # very slow np.array conversion
def sum5(N = N):
# much faster np.array conversion
return np.sum(np.fromiter(range(N),dtype = np.int))
def sum6(N = N):
# possibly slow conversion to Py_long from np.int
return sum(np.arange(N))
def sum7(N = N):
# list returns a list of np.int-s
return sum(list(np.arange(N)))
def sum7v2(N = N):
# tolist conversion to python int seems faster than the implicit conversion
# in sum(list()) (tolist returns a list of python int-s)
return sum(np.arange(N).tolist())
def sum8(N = N):
return np.sum(np.arange(N)) # everything in numpy (fortran libblas?)
def array_basic(N = N):
return np.array(range(N))
def array_dtype(N = N):
return np.array(range(N),dtype = np.int)
def array_iter(N = N):
# np.sum's source code mentions to use fromiter to convert from generators
return np.fromiter(range(N),dtype = np.int)
print(f"while loop: {timeit(sum0, number = repetition)}")
print(f"for loop: {timeit(sum1, number = repetition)}")
print(f"sum_range: {timeit(sum2, number = repetition)}")
print(f"sum_rangelist: {timeit(sum3, number = repetition)}")
print(f"npsum_range: {timeit(sum4, number = repetition)}")
print(f"npsum_fromiterrange:{timeit(sum5, number = repetition)}")
print(f"sum_arange: {timeit(sum6, number = repetition)}")
print(f"sum_list_arange: {timeit(sum7, number = repetition)}")
print(f"sum_arange_tolist: {timeit(sum7v2, number = repetition)}")
print(f"npsum_arange: {timeit(sum8, number = repetition)}")
print(f"array_basic: {timeit(array_basic, number = repetition)}")
print(f"array_dtype: {timeit(array_dtype, number = repetition)}")
print(f"array_iter: {timeit(array_iter, number = repetition)}")
# Example output:
#
# while loop: 9.249794696999743
# for loop: 6.026467555000636
# sum_range: 1.4830789409988938
# sum_rangelist: 3.6745876889999636
# npsum_range: 16.216972655000063
# npsum_fromiterrange:3.47655400199983
# sum_arange: 16.656015603000924
# sum_list_arange: 19.500842117000502
# sum_arange_tolist: 4.004777374000696
# npsum_arange: 0.2332638230000157
# array_basic: 16.1631146109994
# array_dtype: 16.550737804000164
# array_iter: 3.9803170430004684
uj5u.com熱心網友回復:
讓我們看看我是否可以總結結果。
sum可以處理任何可迭代物件,反復詢問下一個值并添加它。 range是一個生成器,很高興提供下一個值
# sum_range: 1.4830789409988938
從范圍中制作串列需要時間:
# sum_rangelist: 3.6745876889999636
對預先生成的串列求和實際上比對范圍求和要快:
%%timeit x = list(range(N))
...: sum(x)
np.sum旨在對陣列求和。它是np.add.reduce.
np.sum有一個棄用警告np.sum(generator),建議使用fromiter或 Python sum:
# npsum_range: 16.216972655000063
fromiter是從生成器制作陣列的最佳方式。使用np.arrayonrange是遺留代碼,將來可能會消失。我認為這是唯一的generator是np.array會接受的。
np.array是一個通用函式,可以處理許多情況,包括嵌套陣列和轉換為各種 dtype。因此,它必須處理整個輸入引數,推匯出 shape 和 dtype。
# npsum_fromiterrange:3.47655400199983
numpy 陣列的迭代比串列慢,因為它必須“拆箱”每個元素。
# sum_arange: 16.656015603000924
同樣,從陣列中創建串列很慢;相同型別的python級別迭代。
# sum_list_arange: 19.500842117000502
arr.tolist()比較快,在編譯代碼中創建一個純python串列。所以速度類似于從范圍內制作串列。
# sum_arange_tolist: 4.004777374000696
np.sum一個陣列是純粹的numpy并且相當快。 np.sum(x)哪里x=np.arange(N)更快(大約 4 倍)
# npsum_arange: 0.2332638230000157
np.sum from range 或 list 由首先創建陣列的成本決定:
# array_basic: 16.1631146109994
# array_dtype: 16.550737804000164
# array_iter: 3.9803170430004684
uj5u.com熱心網友回復:
從sum的cpython 源代碼sum最初似乎嘗試了一條假設所有輸入都是相同型別的快速路徑。如果失敗,它只會迭代:
/* Fast addition by keeping temporary sums in C instead of new Python objects.
Assumes all inputs are the same type. If the assumption fails, default
to the more general routine.
*/
我并不完全確定幕后發生了什么,但很可能是 C 型別到 Python 物件的重復創建/轉換導致了這些減速。值得注意的是,sum和range都是用 C 實作的。
下一點并不是這個問題的真正答案,但我想知道我們是否可以加速sumpython ranges,因為它range是一個非常智能的物件。
為此,我使用functools.singledispatch了專門為該型別覆寫的內置sum函式range;然后實作了一個小函式來計算一個等差數列的總和。
from functools import singledispatch
def sum_range(range_, /, start=0):
"""Overloaded `sum` for range, compute arithmetic sum"""
n = len(range_)
if not n:
return start
return int(start (n * (range_[0] range_[-1]) / 2))
sum = singledispatch(sum)
sum.register(range, sum_range)
def test():
"""
>>> sum(range(0, 100))
4950
>>> sum(range(0, 10, 2))
20
>>> sum(range(0, 9, 2))
20
>>> sum(range(0, -10, -1))
-45
>>> sum(range(-10, 10))
-10
>>> sum(range(-1, -100, -2))
-2500
>>> sum(range(0, 10, 100))
0
>>> sum(range(0, 0))
0
>>> sum(range(0, 100), 50)
5000
>>> sum(range(0, 0), 10)
10
"""
if __name__ == "__main__":
import doctest
doctest.testmod()
我不確定這是否完整,但它絕對比回圈快。
uj5u.com熱心網友回復:
np.sum(range(N))很慢,主要是因為當前的 Numpy 實作沒有使用足夠的關于 generator 提供的值的確切型別/內容的資訊range(N)。一般問題的核心本質上是由于 Python 和大整數的動態型別,盡管 Numpy 可以優化這種特定情況。
首先,range(N)回傳一個動態型別的 Python 物件,它是一種(特殊型別的)Python 生成器。此生成器提供的物件也是動態型別的。它實際上是一個純 Python 整數。
問題是Numpy 是用靜態型別語言 C 撰寫的,因此它不能有效地處理動態型別的純 Python 物件。Numpy 的策略是盡可能將此類物件轉換為 C 型別。在這種情況下,一個大問題是生成器提供的整數在理論上可能很大:Numpy 不知道這些值是否可以溢位一個np.int32甚至一個np.int64型別。因此,Numpy 首先檢測要使用的好型別,然后使用該型別計算結果。
這個轉換程序可能非常昂貴,而且似乎不需要這里,因為range(10_000_000). 然而,range(5_000_000_000)回傳與純Python整數相同的物件型別溢位 np.int32和numpy的需要自動檢測這種情況下不回傳錯誤的結果。問題也是可以正確識別輸入型別(np.int32在我的機器上),這并不意味著輸出結果將是正確的,因為在計算和的程序中可能會出現溢位。可悲的是,我的機器就是這種情況。
Numpy 開發人員決定棄用這種用法并放入np.fromiter應該使用的檔案中。np.fromiter有一個dtype必需的引數讓用戶定義要使用的好型別。
在實踐中檢查這種行為的一種方法是簡單地使用創建一個臨時串列:
tmp = list(range(10_000_000))
# Numpy implicitly convert the list in a Numpy array but
# still automatically detect the input type to use
np.sum(tmp)
更快的實作如下:
tmp = list(range(10_000_000))
# The array is explicitly converted using a well-defined type and
# thus there is no need to perform an automatic detection
# (note that the result is still wrong since it does not fit in a np.int32)
tmp2 = np.array(tmp, dtype=np.int32)
result = np.sum(tmp2)
第一種情況在我的機器上需要 476 毫秒,而第二種情況需要 289 毫秒。請注意,這np.sum僅需要 4 毫秒。因此,大部分時間都花在將純 Python 整數物件轉換為內部 int32 型別(更具體地說是純 Python 整數的管理)上。list(range(10_000_000))也很昂貴,因為它需要 205 毫秒。這又是由于純 Python 整數的開銷(即分配、釋放、參考計數、可變大小整數的增量、記憶體間接和動態型別導致的條件)以及generator的開銷。
sum(np.arange(N))很慢,因為它sum是一個處理 Numpy 定義物件的純 Python 函式。CPython 解釋器需要呼叫 Numpy 函式來執行基本的添加。此外,Numpy 定義的整數物件仍然是 Python 物件,因此它們會受到參考計數、分配、釋放等的影響。更不用說 Numpy 和 CPython 在函式中添加了許多檢查,旨在最終將兩個本地數字相加。支持 Numpy 的即時編譯器(例如 Numba)可以解決此問題。事實上,Numba 在我的機器上需要 23 毫秒來計算np.arange(10_000_000)(代碼仍然用 Python 撰寫)的總和,而 CPython 解釋器需要 556 毫秒。
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/316910.html
上一篇:可以在每次迭代中使用更新變數向量化這個for回圈嗎?
下一篇:foldTree的分步評估
