为什么本机python列表上的for循环比numpy数组上的for循环快

为什么本机python列表上的for循环比numpy数组上的for循环快,python,performance,numpy,for-loop,memory,Python,Performance,Numpy,For Loop,Memory,我正在阅读介绍高性能Python中的numpy的一章,并在自己的计算机上使用代码。我无意中用for循环运行了numpy版本,发现结果比本机python循环慢得惊人 代码的简化版本如下所示,其中我定义了一个2D数组X和0,另一个2D数组Y和1,然后重复地将Y添加到X,概念上X+=Y import time import numpy as np grid_shape = (1024, 1024) def simple_loop_comparison(): xmax, ymax = gri

我正在阅读介绍高性能Python中的numpy的一章,并在自己的计算机上使用代码。我无意中用for循环运行了numpy版本,发现结果比本机python循环慢得惊人

代码的简化版本如下所示,其中我定义了一个2D数组X和0,另一个2D数组Y和1,然后重复地将Y添加到X,概念上X+=Y

import time
import numpy as np

grid_shape = (1024, 1024)

def simple_loop_comparison():
    xmax, ymax = grid_shape

    py_grid = [[0]*ymax for x in range(xmax)]
    py_ones = [[1]*ymax for x in range(xmax)]

    np_grid = np.zeros(grid_shape)
    np_ones = np.ones(grid_shape)

    def add_with_loop(grid, add_grid, xmax, ymax):
        for x in range(xmax):
            for y in range(ymax):
                grid[x][y] += add_grid[x][y]

    repeat = 20
    start = time.time()
    for i in range(repeat):
        # native python: loop over 2D array
        add_with_loop(py_grid, py_ones, xmax, ymax)
    print('for loop with native list=', time.time()-start)

    start = time.time()
    for i in range(repeat):
        # numpy: loop over 2D array
        add_with_loop(np_grid, np_ones, xmax, ymax)
    print('for loop with numpy array=', time.time()-start)

    start = time.time()
    for i in range(repeat):
        # vectorized numpy operation
        np_grid += np_ones
    print('numpy vectorization=', time.time()-start)

if __name__ == "__main__":
    simple_loop_comparison()
结果如下:

# when repeat=10
for loop with native list= 2.545672655105591
for loop with numpy array= 11.622980833053589
numpy vectorization= 0.020279645919799805

# when repeat=20
for loop with native list= 5.195128440856934
for loop with numpy array= 23.241904258728027
numpy vectorization= 0.04613637924194336
我完全希望numpy矢量化操作的性能优于其他两种,但我惊讶地看到,在numpy数组上使用for循环的结果要比原生python列表慢得多。我的理解是,至少缓存应该用numpy数组填充得比较好,即使使用for循环,它也应该比不使用矢量化的list要好

关于numpy或者CPU/缓存/内存如何在低级别工作,是否有我不理解的地方?多谢各位


编辑:更改标题

,因为它们是转换,涉及到向numpy请求数据指针、检索这些指针位置的值,然后使用它们进行迭代。python列表中的这些步骤要少一些。Numpy的速度增益只有在它可以在内部迭代或执行向量、矩阵数学,然后返回并回答或指向答案数组的指针时才会被注意到

因为它们是转换,涉及到向numpy请求数据指针、检索这些指针位置的值,然后使用它们进行迭代。python列表中的这些步骤要少一些。Numpy的速度增益只有在它可以在内部迭代或执行向量、矩阵数学,然后返回并回答或指向答案数组的指针时才会被注意到

对列表和数组的更简单的案例列表理解:

In [119]: x = list(range(1000000))
In [120]: timeit [i for i in x]
47.4 ms ± 634 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [121]: arr = np.array(x)
In [122]: timeit [i for i in arr]
131 ms ± 3.69 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
列表有一个数据缓冲区,其中包含指向内存中其他位置的对象的指针。因此,迭代或索引列表只需要查找该指针并获取对象:

In [123]: type(x[1000])
Out[123]: int
数组将其元素以字节形式存储在数据缓冲区中。获取元素需要快速查找这些字节,然后根据dtype将它们包装到numpy对象中。这样的对象类似于具有许多相同属性的0d单元素数组

In [124]: type(arr[1000])
Out[124]: numpy.int32
这种索引不仅获取数字,还重新创建数字

我经常将对象数据类型数组描述为增强或降级列表。与列表一样,它包含指向内存中其他位置的对象的指针,但不能通过追加来增长。我们常说它失去了数字数组的许多优点。但它的迭代速度介于其他两种之间:

In [125]: arrO = np.array(x, dtype=object)
In [127]: type(arrO[1000])
Out[127]: int
In [128]: timeit [i for i in arrO]
74.5 ms ± 1.42 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
不管怎样,我在其他答案中发现,如果你必须迭代,请坚持使用列表。如果你从列表开始,坚持列表通常会更快。正如您所注意到的,numpy向量速度很快,但是创建数组需要时间,这可能会抵消任何节省的时间

比较从此列表创建数组所需的时间,以及使用编译的numpy代码从头开始创建此类数组所需的时间:

In [129]: timeit np.array(x)
109 ms ± 1.97 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [130]: timeit np.arange(len(x))
1.77 ms ± 31.6 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

与数组相比,列表上更简单的案例列表理解:

In [119]: x = list(range(1000000))
In [120]: timeit [i for i in x]
47.4 ms ± 634 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [121]: arr = np.array(x)
In [122]: timeit [i for i in arr]
131 ms ± 3.69 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
列表有一个数据缓冲区,其中包含指向内存中其他位置的对象的指针。因此,迭代或索引列表只需要查找该指针并获取对象:

In [123]: type(x[1000])
Out[123]: int
数组将其元素以字节形式存储在数据缓冲区中。获取元素需要快速查找这些字节,然后根据dtype将它们包装到numpy对象中。这样的对象类似于具有许多相同属性的0d单元素数组

In [124]: type(arr[1000])
Out[124]: numpy.int32
这种索引不仅获取数字,还重新创建数字

我经常将对象数据类型数组描述为增强或降级列表。与列表一样,它包含指向内存中其他位置的对象的指针,但不能通过追加来增长。我们常说它失去了数字数组的许多优点。但它的迭代速度介于其他两种之间:

In [125]: arrO = np.array(x, dtype=object)
In [127]: type(arrO[1000])
Out[127]: int
In [128]: timeit [i for i in arrO]
74.5 ms ± 1.42 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
不管怎样,我在其他答案中发现,如果你必须迭代,请坚持使用列表。如果你从列表开始,坚持列表通常会更快。正如您所注意到的,numpy向量速度很快,但是创建数组需要时间,这可能会抵消任何节省的时间

比较从此列表创建数组所需的时间,以及使用编译的numpy代码从头开始创建此类数组所需的时间:

In [129]: timeit np.array(x)
109 ms ± 1.97 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [130]: timeit np.arange(len(x))
1.77 ms ± 31.6 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

这不是numpy循环,只是一个带有numpy数组的普通python循环。不管你怎么称呼它,问题是为什么存在绩效差距。双重索引:对于列表列表,您只是引用现有列表和元素;对于二维数组,您是为行和元素创建新对象。2.numpy uuu getitem uuu/uuuuu setitem uuu处理的参数种类比列表计数器的第3部分多得多。我不完全理解其中的区别,但它在列表中
uiltin,array.\uuu getitem\uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu?本地人擅长的东西。。。好的,cache参数是不错的,但是我认为它的影响与每个值的工作量相比是很小的。这不是numpy循环,只是一个带有numpy数组的普通python循环。编辑了标题。不管你怎么称呼它,问题是为什么存在绩效差距。双重索引:对于列表列表,您只是引用现有列表和元素;对于二维数组,您是为行和元素创建新对象。2.numpy uuu getitem uuu/uuuuu setitem uuu处理的参数种类比列表计数器的第3部分多得多。我不完全理解两者之间的区别,但它确实存在。getitem是一个内置的数组。getitem是一个方法包装器,所以你认为非本机的东西会比本机的好?本地人擅长的东西。。。好的,cache参数是不错的,但是我认为它的影响与每个值的工作量相比是很小的。