分配页面对齐的内存块有什么好处?

What are benefits of allocating a page-aligned memory chunk?

我意识到大多数 CPU 更擅长在对齐的内存地址读取数据,即在内存地址是 CPU 字的倍数。但是,在许多地方,我读到有关分配 页面对齐的 内存的信息。为什么有人想要获得页面对齐的内存地址?难道只是为了更好的性能?

"traditional" 分配内存的方法是将它放在一个连续的地址 space 中("heap",通过调用 sbrk() 向上增长)。每次碰到页面边界时,都会出现页面错误,并且您会映射到一个新页面。这种策略有两个后果:

  1. 只有当页面内的所有分配都被释放并且所有其他分配都映射到较低地址时,页面才能被释放。 (堆碎片的典型影响)。
  2. 较大的分配可能会占用比严格需要多的一页(如果它们从页面中间的某处开始)。

因此此策略仅适用于您不想 "waste" 每次分配整个页面的较小内存块。

对于更大的块,最好使用 mmap(),它将您的新页面直接映射到某个地方,因此您会得到 "page aligned memory"。使用它,您的分配不会与其他分配共享页面。一旦您不再需要内存,就可以将其还给 OS。请注意,许多 malloc() 实现会自动选择是使用 sbrk() 还是 mmap() 进行分配,具体取决于所需分配的大小。

对齐限制通常与 direct IO 相关联 - 它绕过页面缓存,将数据 to/from 磁盘直接复制到进程的地址 space 或从进程的地址 space 复制数据。这可以在不需要页面缓存的情况下提供显着的性能改进 - 例如流式传输数 GB 的数据,尤其是在执行 IO to/from 极快的磁盘系统时。

请注意,只有部分文件系统支持直接 IO。

在 Linux 上,RedHat's documentation 部分是:

Direct I/O best practices


Users must always take care to use properly aligned and sized IO. This is especially important for Direct I/O access. Direct I/O should be aligned on a 'logical_block_size' boundary and in multiples of the 'logical_block_size'. With native 4K devices (logical_block_size is 4K) it is now critical that applications perform Direct I/O that is a multiple of the device's 'logical_block_size'. This means that applications that do not perform 4K aligned I/O, but 512-byte aligned I/O, will break with native 4K devices. Applications may consult a device's "I/O Limits" to ensure they are using properly aligned and sized I/O. The "I/O Limits" are exposed through both sysfs and block device ioctl interfaces (also see: libblkid).

sysfs interface

/sys/block//alignment_offset

/sys/block///alignment_offset

/sys/block//queue/physical_block_size

/sys/block//queue/logical_block_size

/sys/block//queue/minimum_io_size

/sys/block//queue/optimal_io_size

请注意,直接 IO 的使用可能会受到实际硬件和软件的限制。如 RedHat 文档中所述,物理设备限制很重要。

要使用直接 IO,在 Linux 上需要使用 O_DIRECT 标志打开文件:

int fd = open( filename, O_RDONLY | O_DIRECT );

根据我的经验,在某些情况下,直接 IO 可以使 IO 性能提高 20-30%。这些情况通常涉及在非常快的文件系统上传输大量数据 to/from 文件,应用程序不执行或执行很少的 seek() 调用。

对齐总是会导致一些性能问题。当你 write(2)read(2) 一个文件时,最好你可以调整你的阅读限制以阻止对齐,因为你让内核做两个块读取而不是一个。最坏的情况是只读取块边界上的 两个 字节。假设你有一个 1024bytes 的块大小,这个代码:

char var[2];
int fd;

fd = open("/etc/passwd", O_RDONLY);
lseek(fd, 1023UL, SEEK_SET);
read(fd, &var, sizeof var);

将使内核强制读取两个块(最多,因为块之前可能已经缓存)仅两个字节 read(2) 调用。

在内存的情况下,所有这些东西通常由 malloc(3) 管理,而且,因为你不会因页面错误而失败,所以你不会得到任何性能损失(这就是你没有任何标准库函数来获得对齐内存的原因,即使在需求分页虚拟系统中也是如此)只要你消耗内存,内核就会为你分配页面。处理器虚拟内存系统使页面对齐几乎是透明的。只有在你有一个未对齐的内存访问的情况下(假设你访问一个 32 位整数访问未对齐 --- 不可能 --- 到两个页面,并且这两个页面已被内核换出,你必须等待内核交换两页内存而不是一页——但这是不太可能发生的事情,编译器通常会强制内部循环在页面边界之间不会失败,以最大限度地减少发生这种情况的可能性,并且您还有指令缓存处理这些事情)

也就是说,如果稍微对齐内存,在某些地方确实可以提高性能。我将尝试向您展示一个这样的场景:

假设您需要动态管理许多小结构(比如说 16 字节长)并且您计划使用 malloc() 来管理它们。 malloc(3) 管理内存,包括分配的每个内存块中的 header(假设这个 header 长 8 个字节),使内存开销增加 50%比理想值多百分之几。如果您安排以(比方说)64 个结构块的形式获取内存,那么每个 64*16 = 1024 字节(总计大约 8%)只会得到其中一个 headers(8 字节)

为了管理这个,你必须考虑知道所有这些结构属于哪个块(这样你可以 free(3) 不使用的块),并且你可以通过两种方式做到这一点:1.-使用指针(向每个结构大小添加 4 个字节——这是没有意义的,因为您将向每个结构添加 4 个字节,再次丢失 25% 的内存)指向 chunck , or 2.- *forcing the chunck to be aligned, so the chunk address can be easily calculated from the struct address (你只需要将除法 mod chunksize 的其余部分减去 struct 地址) 得到块地址。最后一种方法不会强加任何开销来定位块,但会强制所有块的做法是 块对齐(不是页面对齐)。

通过这种方式,您大大提高了性能,因为您大大减少了 malloc(3) 调用的数量以及分配少量内存造成的内存浪费。

顺便说一句,malloc 不会向操作系统询问您在每次调用时询问的内存。它以类似于此处解释的方式分块分配内存,并且正常实现甚至无法再次 return 分配给系统的内存(在分配新内存之前重用释放的内存)它控制调用 sbrk(2) 系统调用,这意味着如果您使用此系统调用,您将干扰 malloc。

当您使用 shmat(2) 系统调用时,

Linux/unix 将为您提供页面对齐的内存。尝试阅读此文档和相关文档。