C++ - 对齐内存

4

我真的想了解这两行代码在发生什么

const int PAGES = 8 * 1024;

// PAGES + extra 4KiB for alignment
uint8_t * mem = new uint8_t [ PAGES * CCPU::PAGE_SIZE + CCPU::PAGE_SIZE ];

// align to a mutiple of 4KiB
uint8_t * memAligned = (uint8_t *) (( ((uintptr_t) mem) + CCPU::PAGE_SIZE - 1) & ~(uintptr_t) ~CCPU::ADDR_MASK );

尤其是最后一行,我对任何东西都不理解...

简而言之:这会浪费内存,你不想要这样的代码 :) - Kuba hasn't forgotten Monica
1
@KubaOber:在某些有限的情况下,页面对齐内存值得浪费;一些操作系统可以通过优化来减少在读取/写入页面对齐内存中的数据时进行的内存复制。如果在高负载 Web 服务器上花费一页内存来获得零拷贝数据传输,那是值得的。 - ShadowRanger
@ShadowRanger 我并不是说你不应该对齐内存,只是你应该通过显式地请求页面对齐的内存来实现它。毕竟,由于“PAGES”很大,支持“new”的分配器将向操作系统请求存储空间。因此,你可能会自己这样做,并以这种方式获得页面对齐的存储空间。在许多甚至大多数情况下,“mem”已经对齐,然后如果它不是const,你可以递增“PAGES”,避免浪费,但我认为不应该鼓励这种hack。 - Kuba hasn't forgotten Monica
@KubaOber:我总体上同意。缺点是它的可移植性较差;你需要编写(至少)POSIX和Windows特定版本的分配器API(在前者上使用mmap映射匿名内存,在后者上使用VirtualAlloc)。虽然不是不可能,但如果你经常这样做,这样做可能是值得的,但我可以理解为什么有人会尝试编写纯C++标准版本的对齐分配器。 - ShadowRanger
1个回答

6
它使用C++分配器来分配一个指向页面对齐内存块的指针,该内存块包含PAGES页,而不是使用更专门的面向操作系统的对齐分配函数(例如POSIX's posix_memalign或C11的aligned_alloc)。首先,它分配PAGES + 1页内存(可能对齐,也可能不对齐),然后将结果指针向前调整,使其指向结果中第一个页面对齐的字节。通过多分配一页,它知道肯定有足够大的分配空间来提供PAGES个可用页面。程序只需要确保在完成时删除mem而不是memAligned(删除后者可能会导致程序立即崩溃,由于堆损坏而在以后崩溃,或者仅泄漏内存;这是未定义的行为,因此将计算机熔化成渣滓是合法行为)。
最后一行等价于将其四舍五入到下一个页面大小的倍数;它将PAGE_SIZE - 1添加到指针中(因此,如果指针已经对齐,则仍在同一页中,否则它将移动到下一页),然后屏蔽地址的低位(这会撤消“已对齐页面”的情况下的加法,并在所有其他情况下将指针重置为mem中不对齐指针所在的第一个页面的开头)。
细节: ~是按位反转运算符,因此ADDR_MASK(可能类似于4096字节页面的0x00000FFF)变为0xFFFFF000(翻转所有位)。&-操作只保留两个操作数中都设置的位。举例说明:对于32位指针,我们假设new给了我们0xDEADBEEF,而PAGE_SIZE为4096。添加4095(0xFFF)意味着我们有'0xDEADCEEE'。然后,我们使用0xFFFFF000进行掩码处理,以消除低位,从而得到0xDEADC000,即跟随0xDEADBEEF的第一个页面对齐地址。对于new返回的任何非页面对齐地址,都会发生相同的情况。
如果已经对齐到页面,比如0xDEADB000,加上 4095/0xFFF 后得到的是 0xDEADBFFF(注意 0xDEADB 中没有任何位发生变化),所以当我们使用掩码获得对齐地址时,我们再次得到了 0xDEADB000,因为我们已经对齐到页面了。
将其转换为uintptr_t是为了确保我们可以使用数学运算符操作地址,并确保按位反转填充所有所需位以匹配指针(如果大小不合适,则可能反转,然后升级,突然间你会在左边而不仅仅是右边有许多零,你最终会屏蔽指针中的重要位,从而指向一个完全不同和错误的地方)。

谢谢,但我更倾向于使用((uintptr_t) mem) + CCPU::PAGE_SIZE - 1) & ~(uintptr_t) ~CCPU::ADDR_MASK。这些疯狂的~是什么意思,为什么我们要重新输入uintptr_t - Charlestone
@Charlestone:我刚刚添加了那个解释。 :-) 现在加上了示例。 - ShadowRanger
哦,我明白了。uintptr_t基本上将地址强制转换为uint32_t(64_t),对吗?还有一件事~(uintptr_t) ~CCPU::ADDR_MASK-为什么这里要双重否定? - Charlestone
1
@Charlestone:啊,错过了双重否定。很可能,ADDR_MASK是一个(可能是32位)值,如0xFFFFF000。但如果系统是64位的,它需要是0xFFFFFFFFFFFFF000。如果你只是将其上转换,你会得到0x00000000FFFFF000,这会破坏任何在上32位设置了位的指针。通过先反转为0x00000FFF,然后上转换添加左手边的0,再反转回来,就像用1位填充左边以匹配系统对指针大小的概念,无论指针有多大。 - ShadowRanger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接