【发布时间】:2015-11-15 07:46:08
【问题描述】:
我意识到大多数 CPU 更擅长在对齐的内存地址读取数据,即内存地址是 CPU 字的倍数。然而,我在很多地方读到了关于分配 page-aligned 内存的内容。为什么有人想要获得页面对齐的内存地址?只是为了获得更大的性能吗?
【问题讨论】:
标签: c memory-management alignment cpu
我意识到大多数 CPU 更擅长在对齐的内存地址读取数据,即内存地址是 CPU 字的倍数。然而,我在很多地方读到了关于分配 page-aligned 内存的内容。为什么有人想要获得页面对齐的内存地址?只是为了获得更大的性能吗?
【问题讨论】:
标签: c memory-management alignment cpu
对齐总是会产生一些性能问题。当您 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) 管理内存,包括分配的每个内存块中的标头(假设此标头长 8 个字节),使内存开销比理想值高出 50%。如果您安排以(比如说)64 个结构块的形式获取内存,那么每个 64*16 = 1024 字节(大约 8%)只会获得其中一个标头(8 字节)
要管理这个,你必须考虑知道所有这些结构属于哪个块(这样你可以在不使用时释放(3)块),你可以分两步完成方法: 1.- 使用指针(向每个结构大小添加 4 个字节——这是没有意义的,因为您将向每个结构添加 4 个字节,再次丢失 25% 的内存)指向块,或 2.- *强制chunck对齐,所以可以很容易的从struct地址计算出chunk地址(只需要将除法mod chunksize的其余部分减去struct address)就可以得到chunk地址。最后一种方法不会施加任何开销来定位块,但会强制所有块的做法是块对齐(不是页面对齐)。
通过这种方式,您可以极大地提高性能,因为您大大减少了 malloc(3) 调用的数量以及分配少量内存所造成的内存浪费。
顺便说一句,malloc 不会向操作系统询问您在每次调用时向它询问的内存。它以与此处解释的方式类似的方式以块的形式分配内存,并且正常的实现甚至无法再次将分配的内存返回给系统(在分配新内存之前重用已释放的内存)它控制对 sbrk(2) 系统调用,这意味着如果你使用这个系统调用,你会干扰 malloc。
Linux/unix 会在您使用 shmat(2) 系统调用时为您提供页面对齐的内存。尝试阅读此文档和相关文档。
【讨论】:
对齐限制通常与direct IO 相关联 - 它绕过页面缓存,将数据直接复制到磁盘或从磁盘复制到进程的地址空间或从进程的地址空间复制数据。在不需要页面缓存的情况下,这可以显着提高性能 - 例如流式传输数 GB 的数据,尤其是在与极快的磁盘系统之间进行 IO 时。
请注意,只有部分文件系统支持直接 IO。
在 Linux 上,RedHat's documentation 部分是:
直接 I/O 最佳实践
用户必须始终注意使用正确对齐和调整大小的 IO。 这对于直接 I/O 尤其重要 使用权。直接 I/O 应在“logical_block_size”上对齐 边界和'logical_block_size'的倍数。原生 4K 设备(logical_block_size 为 4K)现在至关重要的是 应用程序执行直接 I/O 是设备的倍数 “逻辑块大小”。这意味着应用程序不 执行 4K 对齐的 I/O,但 512 字节对齐的 I/O,会中断 原生 4K 设备。应用程序可以查阅设备的“I/O 限制” 以确保他们使用正确对齐和大小合适的 I/O。 “输入/输出 限制”通过 sysfs 和块设备 ioctl 公开 接口(另见:libblkid)。
sysfs 接口
/sys/block//alignment_offset
/sys/block///alignment_offset
/sys/block//queue/physical_block_size
/sys/block//队列/logical_block_size
/sys/block//队列/minimum_io_size
/sys/block//队列/optimal_io_size
请注意,直接 IO 的使用可能会受到实际硬件和软件的限制。如 RedHat 文档中所述,物理设备限制很重要。
要使用直接 IO,在 Linux 上需要使用O_DIRECT 标志打开文件:
int fd = open( filename, O_RDONLY | O_DIRECT );
根据我的经验,在某些情况下,直接 IO 可以使 IO 性能提高 20-30%。这些情况通常涉及将大量数据流式传输到速度非常快的文件系统上的文件/从文件流式传输,而应用程序不执行或执行很少的seek() 调用。
【讨论】:
分配内存的“传统”方式是将其放在一个连续的地址空间(“堆”,通过调用sbrk() 向上增长)。每次遇到页面边界时,都会出现页面错误,并且您会映射一个新页面。这种策略有两个后果:
所以这个策略只适用于你不想为每次分配“浪费”一整个页面的较小的内存块。
对于更大的块,最好使用mmap() 直接将新页面映射到某个地方,这样你就可以获得“页面对齐内存”。使用它,您的分配不会与其他分配共享页面。一旦不再需要内存,就可以将其还给操作系统。请注意,许多malloc()实现会根据所需分配的大小自动选择是使用sbrk()还是mmap()进行分配。
【讨论】:
mmap() 分配这些大块(因此是页面对齐的)确保free() 真正将页面释放到操作系统。
mmap() 的粒度是整页 [请求单个字节,获取一页]。这就是为什么您不想为小分配执行此操作 - 传统的 sbrk() 更适合它们。