内存对齐#

NumPy 的对齐目标#

NumPy 中有三个与内存对齐相关的用例(截至 1.14 版本)

  1. 创建结构化数据类型,其字段对齐方式类似于 C 语言结构体。

  2. 通过使用 uint 赋值代替 memcpy 来加速复制操作。

  3. 确保 ufuncs/setitem/转换代码的安全对齐访问。

NumPy 使用两种不同的对齐形式来实现这些目标:“真实对齐(True alignment)”和“Uint 对齐(Uint alignment)”。

“真实”对齐是指 C 语言中等效 C 类型随架构变化的对齐方式。例如,在 x64 系统中,float64 等同于 C 语言中的 double。在大多数系统上,其对齐方式为 4 或 8 字节(可以通过 GCC 选项 malign-double 进行控制)。如果变量的内存偏移量是其对齐值的倍数,则称该变量在内存中是对齐的。在某些系统(例如 sparc)上,强制要求内存对齐;而在其他系统上,对齐可以提高运行速度。

“Uint”对齐取决于数据类型的大小。它被定义为 NumPy 复制代码用于复制该数据类型时所使用的 uint 的“真实对齐”值;如果没有等效的 uint,则为未定义/未对齐。目前,NumPy 分别使用 uint8uint16uint32uint64uint64 来复制大小为 1、2、4、8、16 字节的数据,所有其他大小的数据类型都无法进行 uint 对齐。

例如,在(典型的 Linux x64 GCC)系统上,NumPy 的 complex64 数据类型实现为 struct { float real, imag; }。它的“真实”对齐为 4,“Uint”对齐为 8(等于 uint64 的真实对齐)。

Uint 对齐与真实对齐不同的某些情况(默认 GCC Linux)

架构

类型

真实对齐 (true-aln)

Uint 对齐 (uint-aln)

x86_64

complex64

4

8

x86_64

float128

16

8

x86

float96

4

-

NumPy 中用于控制和描述对齐的变量#

NumPy 中有 4 种与 align 一词相关的用途

  • dtype.alignment 属性(C 语言中的 descr->alignment)。这旨在反映类型的“真实对齐”。除了下面描述的用 align=True 创建的结构化类型外,所有数据类型都有依赖于架构的默认值。

  • ndarray 的 ALIGNED 标志,在 IsAligned 中计算并由 PyArray_ISALIGNED 检查。这是根据 dtype.alignment 计算得出的。如果数组中的每个项都位于与 dtype.alignment 一致的内存位置,则设置为 True。如果 data ptr 和数组的所有步长(strides)都是该对齐值的倍数,则满足此条件。

  • dtype 构造函数的 align 关键字,仅影响结构化数组。如果不手动提供结构的字段偏移量,NumPy 会自动确定偏移量。在这种情况下,align=True 会填充结构,以便每个字段在内存中都是“真实”对齐的,并将 dtype.alignment 设置为字段“真实”对齐值中的最大值。这与 C 结构体通常的做法类似。否则,如果手动提供了偏移量或 itemsize,align=True 仅检查所有字段是否都已“真实”对齐,且总 itemsize 是否为最大字段对齐值的倍数。无论哪种情况,dtype.isalignedstruct 也会设置为 True。

  • IsUintAligned 用于判断 ndarray 是否为“uint 对齐”,其方式与 IsAligned 检查真实对齐的方式类似。

对齐的后果#

上述变量的使用方式如下:

  1. 创建对齐结构:当 align=True 时,为了知道如何偏移字段,NumPy 会查找 field.dtype.alignment。这包括作为嵌套结构化数组的字段。

  2. Ufuncs:如果数组的 ALIGNED 标志为 False,ufuncs 将在评估前对数组进行缓冲/转换。这是必要的,因为 ufunc 内部循环直接访问原始元素,如果元素没有真实对齐,这在某些架构上可能会失败。

  3. Getitem/setitem/copyswap 函数:与 ufuncs 类似,这些函数通常有两条代码路径。如果 ALIGNED 为 False,它们将使用缓冲参数的代码路径,以确保它们是真实对齐的。

  4. 步长复制代码:此处改为使用“uint 对齐”。如果数组的 itemsize 等于 1、2、4、8 或 16 字节,且数组是 uint 对齐的,则 NumPy 将执行 *(uintN*)dst = *(uintN*)src(对于适当的 N)。否则,NumPy 通过 memcpy(dst, src, N) 进行复制。

  5. Nditer 代码:由于此代码经常调用步长复制代码,因此必须检查“uint 对齐”。

  6. 转换代码:这会检查“真实”对齐,因为如果对齐,它会执行 *dst = CASTFUNC(*src)。否则,它会执行 memmove(srcval, src); dstval = CASTFUNC(srcval); memmove(dst, dstval),其中 dstval/srcval 是对齐的。

请注意,步长复制和步长转换代码深度交织在一起,因此由它们处理的任何数组都必须同时进行 uint 对齐和真实对齐,尽管复制代码只需要 uint 对齐,而转换代码只需要真实对齐。如果将来对这些代码进行大规模重构,允许它们使用不同的对齐方式将会很有意义。