首页电影内存和性能

内存和性能

paiquba 01-01 22次浏览 0条评论

本节将介绍 numpy 数组中的内存布局。

内存和性能

%pylab inline我们从一个例子开始 - 总结一个数组的行或列。每个函数的计算复杂度完全一样:n 。

n = 2000x = np.random.rand(n, n)%timeit x[0].sum()5.58 µs ± 372 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)%timeit x[:,0].sum()20.4 µs ± 1.47 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)请注重,循环运行所需的时间存在显着差异。对行求和更快

x = np.array(x, order='F')%timeit x[0].sum()17.4 µs ± 1.48 µs per loop (mean ± std. dev. of 7 runs, 100000 loops each)%timeit x[:,0].sum()7.35 µs ± 478 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)这个时候,我们发现,现在对列求和更快。

为什么这个order很重要?

物理内存¶计算机上有多个数据存储位置:

硬盘/SSD O(TB)随机存取存储器 O(10GB)三级缓存 O(10MB)二级缓存 O(100KB)一级缓存 O(64KB)CPU(寄存器)在这个序列的前边,有足够大的存储空间,但是访问数据需要更长的时间。这个序列往下,可用存储量减少,但我们能够更快地访问数据。我们可以在 1 个时钟周期内访问 CPU 寄存器,但我们只能保存少量(例如 8 个)64 位浮点数。

当你遍历一个数组时,内存会从一个位置复制到序列中的下一个。通常你会从 RAM 开始,在那里生成数据或从磁盘加载。

数组中的每个元素都有一个唯一的地址(在 RAM 中)——对于 64 位架构,每个地址都对应一个 64 位(8 字节)字。这将恰好保存一个 64 位浮点数。

通常,数组存储在连续的内存块中,这意味着第一个元素存储在某个地址a,第二个元素存储在该地址a+1(64 位之后),通常第 i 个元素存储在地址a+i

一个数组会包含一个指向数据开始的指针(即数组开始的地址),当你得到一个数组的元素x[i]时,首先查找起始地址a,然后在地址中查找数据a+i.

x = np.arange(5)print(x)a = x.data # 数组的起始地址print(a)[0 1 2 3 4]<memory at 0x000002B7A07BC648> 0x40 = 64

地址

数据

0x00

0x40

1

0x80

2

0xb0

3

当你访问某个内存元素以在 CPU 中进行计算时,不仅仅是将那个地址移动到缓存中,而是将整个内存块移动到该地址四周。这样,当你在查看下一个地址时,它会被预加载到缓存中,你将能够更快地访问该地址中的数据。

当你要访问的地址未加载到缓存中时,称为缓存未命中,需要额外的时间将该内存移动到缓存,然后再移动到 CPU。最小化缓存未命中数的代码将比最大化缓存未命中数的代码快得多。

这就是为什么按内存顺序循环数组比按不同顺序循环要快得多的原因。

二维数组¶到目前为止,我们所说的一切对于一维数组都相当简单。多维数组呢?

我们考虑二维数组,当然这些想法可以妥善到更高的维度。

二维数组有两个索引,因此最多可以使用其中一个索引来访问内存中的相邻地址。假如我们将二维数组的行(第一个索引)存储在连续内存中,我们说该数组是行主格式。假如我们将二维数组的列(第二个索引)存储在连续内存中,我们说该数组是列主格式。

C/C++ 和 Python 以行主格式存储多维数组

Fortran、Matlab、R 和 Julia 以列主格式存储多维数组

因为数值库通常是用 C/C++ 或 Fortran 编写的,所以在科学计算中不必担心行与列的主格式。但是,出于各种原因,列主格式被视为默认设置。

Numpy 支持行和列主格式,但默认情状下是行主格式。你可以通过访问该flags字段来查看

x = np.random.rand(3,3)x.flags C_CONTIGUOUS : True F_CONTIGUOUS : False OWNDATA : True WRITEABLE : True ALIGNED : True WRITEBACKIFCOPY : False UPDATEIFCOPY : FalseC_CONTIGUOUS(C 默认) 是指行主格式。 F_CONTIGUOUS(fortran 默认)是指列主要。

order数组创建中的标志可用于对此的操作。

print("Row major:")x = np.array(np.random.rand(3,3), order='C')print(x.flags)print("Column major:")x = np.array(np.random.rand(3,3), order='F')print(x.flags)Row major: C_CONTIGUOUS : True F_CONTIGUOUS : False OWNDATA : True WRITEABLE : True ALIGNED : True WRITEBACKIFCOPY : False UPDATEIFCOPY : FalseColumn major: C_CONTIGUOUS : False F_CONTIGUOUS : True OWNDATA : True WRITEABLE : True ALIGNED : True WRITEBACKIFCOPY : False UPDATEIFCOPY : False

Numba 示例¶内存存储对您可能期看如何遍历数组有影响。我们将使用 Numba 来演示这一点,因为原始 Python 循环的消耗较大,无法看到明显的差异。

from numba import jit@jit(nopython=True) # throws error if not able to compiledef numba_matvec_row(A, x): """ naive matrix-vector multiplication implementation Loops over rows in outer loop """ m, n = A.shape y = np.zeros(m) for i in range(m): for j in range(n): y[i] = y[i] + A[i,j] * x[j] return y@jit(nopython=True) # throws error if not able to compiledef numba_matvec_col(A, x): """ naive matrix-vector multiplication implementation Loops over columns in outer loop """ m, n = A.shape y = np.zeros(m) for j in range(n): for i in range(m): y[i] = y[i] + A[i,j] * x[j] return ym = 4000n = 4000A = np.array(np.random.randn(m,n), order='C')x = np.random.randn(n)# precompiley = numba_matvec_row(A, x)y = numba_matvec_col(A, x)%timeit y = numba_matvec_row(A, x)25 ms ± 321 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)%timeit y = numba_matvec_col(A, x)136 ms ± 3.72 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)m = 4000n = 4000A = np.array(np.random.randn(m,n), order='F')x = np.random.randn(n)# precompiley = numba_matvec_row(A, x)y = numba_matvec_col(A, x)%timeit y = numba_matvec_row(A, x)140 ms ± 15.2 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)%timeit y = numba_matvec_col(A, x)9.43 ms ± 966 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)对于A,在列主顺序中看到了更大的差异。是否可以改良行主要函数。我们使用 NumPy 的dot函数来明确利用连续行设置。(一般情状,Numba 与内置的 NumPy 函数兼容)。

@jit(nopython=True) # throws error if not able to compiledef numba_matvec_row2(A, x): """ naive matrix-vector multiplication implementation Loops over rows in outer loop """ m, n = A.shape y = np.zeros(m) for i in range(m): y[i] = np.dot(A[i], x) # takes explicit advantage of the contigous row layout return ym = 4000n = 4000A = np.array(np.random.randn(m,n), order='C')x = np.random.randn(n)# precompiley = numba_matvec_row(A, x)y = numba_matvec_row2(A, x)%timeit y = numba_matvec_row(A, x)27.1 ms ± 1.53 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)%timeit y = numba_matvec_row2(A, x)8.84 ms ± 495 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)现在,我们终于看到了加速。

再与 Numpy 比较看看

%timeit y = np.matmul(A, x)6.15 ms ± 684 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)Numba 仍然没有那么快。让我们看看是否可以使用一些并行性来扶助我们。

from numba import prange@jit(nopython=True, parallel=True) # throws error if not able to compiledef numba_matvec_row3(A, x): """ naive matrix-vector multiplication implementation Loops over rows in outer loop """ m, n = A.shape y = np.zeros(m) for i in prange(m): y[i] = np.dot(A[i], x) # takes explicit advantage of the contigous row layout return ym = 4000n = 4000A = np.array(np.random.randn(m,n), order='C')x = np.random.randn(n)# precompiley = numba_matvec_row3(A, x)%timeit y = numba_matvec_row3(A, x)9.64 ms ± 1.25 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)%timeit y = np.matmul(A, x)5.78 ms ± 218 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)现在可以得出结论,A以不同的顺序循环矩阵可以产生显着的差异。然而,即使使用 Numba,也很难击败 NumPy 的内置函数。

随机存取
需求增加 全球服务器DRAM今年价格涨幅或达40% 需求强劲DRAM价格今年暴涨,交通运输部积极推动碳达峰丨明日主题前瞻
相关内容
发表评论

游客 回复需填写必要信息