内存对齐#

NumPy 对齐目标#

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

  1. 创建结构化数据类型,其中字段按照 C 结构体的对齐方式对齐。

  2. 通过使用uint 赋值而不是memcpy来加速复制操作。

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

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)

架构

类型

真对齐

uint 对齐

x86_64

complex64

4

8

x86_64

float128

16

8

x86

float96

4

-

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

NumPy 中有 4 种与单词align相关的用法

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

  • ndarray 的ALIGNED标志,在IsAligned中计算并由PyArray_ISALIGNED检查。它根据dtype.alignment计算。如果数组中的每个项都在与dtype.alignment一致的内存位置上,则该标志设置为True,当数组的data ptr和所有步长都是该对齐值的倍数时即是这种情况。

  • dtype 构造函数的align关键字,它只影响结构化数组。如果未手动提供结构的字段偏移量,NumPy 会自动确定偏移量。在这种情况下,align=True会填充结构,使每个字段在内存中“真”对齐,并将dtype.alignment设置为字段“真”对齐中的最大值。这与 C 结构体通常的做法类似。否则,如果手动提供了偏移量或项大小,align=True只会检查所有字段是否“真”对齐,以及总项大小是否为最大字段对齐值的倍数。在任何一种情况下,dtype.isalignedstruct也会被设置为 True。

  • IsUintAligned用于以与IsAligned检查真对齐类似的方式,确定 ndarray 是否“uint 对齐”。

对齐的后果#

以上变量的使用方式如下

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

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

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

  4. 步幅复制代码:这里,改为使用“uint 对齐”。如果数组的项大小等于 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 对齐,而转换代码只需要真对齐。如果将来对这段代码进行大规模重写,最好允许它们使用不同的对齐方式。