Python 将NumPy数组转换为集合需要的时间太长

Python 将NumPy数组转换为集合需要的时间太长,python,arrays,performance,numpy,set,Python,Arrays,Performance,Numpy,Set,我正在尝试执行以下命令 from numpy import * x = array([[3,2,3],[711,4,104],.........,[4,4,782,7845]]) # large nparray for item in x: set(item) 相比之下,这需要很长时间: x = array([[3,2,3],[711,4,104],.........,[4,4,782,7845]]) # large nparray for item in x: item.

我正在尝试执行以下命令

from numpy import *
x = array([[3,2,3],[711,4,104],.........,[4,4,782,7845]])  # large nparray
for item in x:
    set(item)
相比之下,这需要很长时间:

x = array([[3,2,3],[711,4,104],.........,[4,4,782,7845]])  # large nparray
for item in x:
    item.tolist()
为什么将NumPy数组转换为
集合
比转换为
列表
需要更长的时间? 我的意思是基本上两者都有复杂性
O(n)

TL;DR:
set()
函数使用Pythons迭代协议创建一个集合。但是(在Python级别上)在NumPy数组上的迭代非常慢,因此在进行迭代之前使用
tolist()
将数组转换为Python列表的速度(要快得多)

要理解为什么在NumPy数组上迭代如此之慢,了解Python对象、Python列表和NumPy数组如何存储在内存中是很重要的

Python对象需要一些簿记属性(如引用计数、指向其类的链接等)及其表示的值。例如,整数
ten=10
可以如下所示:

蓝色圆圈是Python解释器中用于变量
ten
的“名称”,下面的对象(实例)实际上代表整数(因为簿记属性在这里并不重要,我在图像中忽略了它们)

Python
list
只是Python对象的集合,例如
mylist=[1,2,3]
将按如下方式保存:

这次列表引用了Python整数
1
2
3
,而名称
mylist
只引用了
list
实例

但是数组
myarray=np.array([1,2,3])
不将Python对象存储为元素:

1
2
3
直接存储在NumPy
array
实例中


有了这些信息,我可以解释为什么在
数组上迭代比在
列表上迭代要慢得多:

arr = np.arange(100000)

%timeit list(arr)
# 20 ms ± 114 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit arr.tolist()
# 10.3 ms ± 253 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
arr = np.random.randint(0, 1000, (10000, 3))

def tosets(arr):
    for line in arr:
        set(line)

def tolists(arr):
    for line in arr:
        list(line)

def tolists_method(arr):
    for line in arr:
        line.tolist()

def tosets_intermediatelist(arr):
    for line in arr:
        set(line.tolist())

%timeit tosets(arr)
# 72.2 ms ± 2.68 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit tolists(arr)
# 80.5 ms ± 2.18 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit tolists_method(arr)
# 16.3 ms ± 140 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit tosets_intermediatelist(arr)
# 38.5 ms ± 200 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
每次访问
列表中的下一个元素时,
列表只返回一个存储的对象。这非常快,因为元素已经作为Python对象存在(它只需要将引用计数增加1)

另一方面,当您需要
数组的元素时,它需要为该值创建一个新的Python“box”,其中包含所有簿记内容,然后才能返回该值。迭代数组时,需要为数组中的每个元素创建一个Python框:

创建这些框的速度很慢,而且在NumPy数组上迭代比在Python集合(列表/元组/集合/字典)上迭代慢得多的主要原因是Python集合存储值及其框:

import numpy as np
arr = np.arange(100000)
lst = list(range(100000))

def iterateover(obj):
    for item in obj:
        pass

%timeit iterateover(arr)
# 20.2 ms ± 155 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit iterateover(lst)
# 3.96 ms ± 26.6 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
集合
“构造函数”只是在对象上进行迭代

有一件事我不能肯定地回答,那就是为什么
tolist
方法要快得多。最后,结果Python列表中的每个值都需要放在一个“Python框”中,因此
tolist
可以避免的工作量不多。但有一件事我可以肯定,那就是
list(array)
array.tolist()慢:

每种方法都有运行时复杂性,但常数因子是非常不同的

在您的例子中,您确实比较了
set()
tolist()
——这不是一个特别好的比较。将
set(arr)
list(arr)
set(arr.tolist())
arr.tolist()进行比较更有意义:

因此,如果您想要
set
s,最好使用
set(arr.tolist())
。对于更大的数组,使用它可能是有意义的,但因为您的行只包含3个可能会更慢的项(对于数千个元素,它可能会更快!)


在你询问的关于numba的评论中,是的,numba确实可以加快速度,但这并不意味着它总是更快

我不确定numba(重新)如何实现
set
s,但由于它们是类型化的,因此它们可能也会避免使用“Python框”,并将值直接存储在
set
中:

集合比
list
s更复杂,因为它们涉及
hash
es和空槽(Python对集合使用开放寻址,所以我假设numba也会)

与NumPy
array
一样,numba
set
直接保存值。因此,当您将NumPy
数组
转换为numba
(或反之亦然)时,根本不需要使用“Python框”,因此当您在numbanopython函数中创建
时,它甚至比
集(arr.tolist())
操作快得多:

import numba as nb
@nb.njit
def tosets_numba(arr):
    for lineno in range(arr.shape[0]):
        set(arr[lineno])

tosets_numba(arr)  # warmup
%timeit tosets_numba(arr)
# 6.55 ms ± 105 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
这大约是
set(arr.tolist())
方法的五倍。但需要强调的是,我没有从函数中返回
s。当您返回一个
集合
从nopython numba函数到Python时,numba将创建一个Python集合,包括为集合中的所有值“创建框”(这是numba隐藏的内容)

仅FYI:如果将
list
s传递给Numba noython函数或这些函数的返回列表,则会发生相同的装箱/取消绑定。因此,Python中的
O(1)
操作是使用Numba的
O(n)
操作!这就是为什么通常最好将NumPy数组传递给numba nopython函数(即
O(1)

我假设如果从函数中返回这些集(因为Nuba当前不支持集合列表,所以现在不可能),那么速度会慢一些(因为它创建了一个Nuba集,并且以后将其转换为python集),或者只会稍微快一些(如果转换numbaset->pythonset非常非常快)

就我个人而言,只有当我不需要从函数返回集合并在集合内部执行所有操作时,我才会对集合使用numba
from multiprocessing.dummy import Pool as ThreadPool
import multiprocessing

pool = ThreadPool(multiprocessing.cpu_count()) # get the number of CPU
y = pool.map(set,x) # apply the function to your iterable
pool.close()
pool.join()