内存复制例程可以比通过指针进行的简单内存复制更复杂和更快,例如:
void simple_memory_copy(void* dst, void* src, unsigned int bytes)
{
unsigned char* b_dst = (unsigned char*)dst;
unsigned char* b_src = (unsigned char*)src;
for (int i = 0; i < bytes; ++i)
*b_dst++ = *b_src++;
}
改进
可以做的第一个改进是在字边界上对齐一个指针(我的意思是本机整数大小,通常是 32 位/4 字节,但在较新的架构上可以是 64 位/8 字节)并使用字大小的移动/复制指令。这需要使用字节到字节的复制,直到指针对齐。
void aligned_memory_copy(void* dst, void* src, unsigned int bytes)
{
unsigned char* b_dst = (unsigned char*)dst;
unsigned char* b_src = (unsigned char*)src;
// Copy bytes to align source pointer
while ((b_src & 0x3) != 0)
{
*b_dst++ = *b_src++;
bytes--;
}
unsigned int* w_dst = (unsigned int*)b_dst;
unsigned int* w_src = (unsigned int*)b_src;
while (bytes >= 4)
{
*w_dst++ = *w_src++;
bytes -= 4;
}
// Copy trailing bytes
if (bytes > 0)
{
b_dst = (unsigned char*)w_dst;
b_src = (unsigned char*)w_src;
while (bytes > 0)
{
*b_dst++ = *b_src++;
bytes--;
}
}
}
根据源指针或目标指针是否适当对齐,不同的体系结构将执行不同的操作。例如,在 XScale 处理器上,通过对齐目标指针而不是源指针,我获得了更好的性能。
为了进一步提高性能,可以进行一些循环展开,以便更多的处理器寄存器加载数据,这意味着加载/存储指令可以交错并通过附加指令(例如循环计数等)隐藏它们的延迟)。这带来的好处因处理器而异,因为加载/存储指令延迟可能完全不同。
在这个阶段,代码最终是用汇编而不是 C(或 C++)编写的,因为您需要手动放置加载和存储指令以获得最大的延迟隐藏和吞吐量优势。
通常应在展开循环的一次迭代中复制整个缓存行数据。
这让我想到了下一个改进,即添加预取。这些是告诉处理器的缓存系统将内存的特定部分加载到其缓存中的特殊指令。由于发出指令和填充高速缓存行之间存在延迟,因此需要以这样一种方式放置指令,以便数据在复制时可用,而不是迟早。
这意味着将预取指令放在函数的开头以及主复制循环内。使用复制循环中间的预取指令获取将在多次迭代时间内复制的数据。
我不记得了,但预取目标地址和源地址也可能是有益的。
因素
影响内存复制速度的主要因素有:
- 处理器、其缓存和主内存之间的延迟。
- 处理器缓存线的大小和结构。
- 处理器的内存移动/复制指令(延迟、吞吐量、寄存器大小等)。
因此,如果您想编写一个高效且快速的内存处理例程,您将需要对所编写的处理器和架构有很多了解。可以这么说,除非您在某些嵌入式平台上编写,否则仅使用内置的内存复制例程会容易得多。