chr*_*oph 9 python arrays numpy
在使用 Python 和 Numpy 实现一些 Gauss-Seidel 求解器时,我发现了一个有趣的副作用。我试图提取一些最小的例子:
    number = 1000
    startup = 'import numpy as np;N = 2048;X = np.arange(N * N).reshape((N, N));'
    startup2 = startup + 'np.empty_like(X)'
    example = ('X[1:: 2, : -1: 2] = ('
               'X[: -1: 2, 1:: 2] +'
               'X[1:: 2, : -1: 2] +'
               'X[1:: 2, : -1: 2] +'
               'X[: -1: 2, : -1: 2])/4')
print(timeit.timeit(example, setup=startup, number=number))在我的机器上运行~5s
而print(timeit.timeit(example, setup=startup2, number=number))需要~4s。
所以大约快 1 秒,尽管np.emtpy_like(X). 我在各种机器和各种数组大小或迭代中观察到了这种效果。
我假设分配中右侧的计算会导致时间数组分配。似乎 Numpy 以某种方式重用了创建的未使用数组np.emtpy_like(X)以加快时间数组分配。
我的这个假设是否正确,还是时间差异的原因完全不同?
如果我删除/4到
 example = ('X[1:: 2, : -1: 2] = ('
               'X[: -1: 2, 1:: 2] +'
               'X[1:: 2, : -1: 2] +'
               'X[1:: 2, : -1: 2] +'
               'X[: -1: 2, : -1: 2])')
然后,我无法观察到不同版本之间执行时间的差异。所以我假设在这种情况下计算可以就地完成,然后没有时间分配。
有没有更明确的方法来利用这种效果?只是写 np.emtpy_like(X)` 在我看来有点“hacky”。
提前致谢!
更新:
感谢您到目前为止的回答和评论。
最后我找到了更多时间来处理我的观察,我不确定我上面的解释是否足够清楚。所以我最初的观察是,这
 example = ('X[1:: 2, : -1: 2] = ('
               'X[: -1: 2, 1:: 2] +'
               'X[1:: 2, : -1: 2] +'
               'X[1:: 2, : -1: 2] +'
               'X[: -1: 2, : -1: 2])')
比这更快
    number = 1000
    N = 1024
    X = np.arange(N * N).reshape((N, N))
    np.empty_like(X)
    for _ in range(number):
        X[1:: 2, : -1: 2] = (X[: -1: 2, 1:: 2] + X[1:: 2, : -1: 2] +
                             X[1:: 2, : -1: 2] + X[: -1: 2, : -1: 2]) / 4
这让我感到很惊讶,因为这种完全未使用且不必要的数组分配np.empty_like(X)似乎加速了下面的循环。因此,它并不重要,如果我用np.empty_like,zeros_like,ones_like,ones(X.shape)或者X.copy()只要有分配的未使用的阵列。它也发生在不同的 N、迭代次数和不同的机器上。
此外,我试图调查tracemalloc包的问题:
    number = 1000
    N = 1024
    X = np.arange(N * N).reshape((N, N))
    for _ in range(number):
        X[1:: 2, : -1: 2] = (X[: -1: 2, 1:: 2] + X[1:: 2, : -1: 2] +
                             X[1:: 2, : -1: 2] + X[: -1: 2, : -1: 2]) / 4
其中 display_top 是文档中的方法,除了我以字节而不是 KB 打印出来。
当我在没有额外数组分配的情况下运行它时,np.empty_like(X)我会得到一些这样的输出:
Top 10 lines
#1: ./minexample.py:40: 160.0 B
    X[1:: 2, : -1: 2] = (X[: -1: 2, 1:: 2] + X[1:: 2, : -1: 2] + X[1:: 2, : -1: 2] + X[: -1: 2, : -1: 2]) / 4
#2: ./minexample.py:39: 28.0 B
    for _ in range(1000):
Total allocated size: 188.0 B
有了额外的分配,我得到了这个:
Top 10 lines
#1: ./minexample.py:40: 128.0 B
    X[1:: 2, : -1: 2] = (X[: -1: 2, 1:: 2] + X[1:: 2, : -1: 2] + X[1:: 2, : -1: 2] + X[: -1: 2, : -1: 2]) / 4
#2: ./minexample.py:38: 32.0 B
    np.empty_like(X)
#3: ./minexample.py:39: 28.0 B
    for _ in range(1000):
Total allocated size: 188.0 B
因此,当没有预先分配未使用的数组时,循环中分配的行的大小要小。所以在我看来,这个未使用的数组被重用了。
也许这可以解释这种效果?
empty_like(X)初始化一个具有相同维度的数组(或矩阵),并用随机值X填充它,这是一个非常快的过程。但请记住,这不会给你想要的结果(你需要的)!+ empty_like(X)zeros_like(X)
为什么在 numpy 中添加两个数组很快与密集排列的数组有关,如下所述: https: //stackoverflow.com/a/8385658/14344821,这可能比X使用明确提到的条目创建矩阵更快。有关如何有效创建 numpy 数组的提示可以在此处找到。