回答策略
- 首先,写出能正确处理重叠问题的版本。
- 然后,提出块拷贝优化,并主动点出按块访问连续内存的前提------这块内存的首地址必须是按
sizeof(uintptr_t)
个字节对齐的。 - 接着,讨论"对齐同步性"问题(即使用块拷贝优化前必须确保dst和src指针均已按
sizeof(uintptr_t)
个字节对齐)。并解释如果不满足这个条件,你能想到的最简单的做法是回退到逐字节拷贝。 - 最后,可以提及真正的库函数会使用平台相关的汇编/SIMD指令来解决这个问题,这会展示你知识的深度。
参考代码
C++
#include <iostream>
#include <cassert>
void* MemoryMove(void* dst, const void* src, size_t n) {
if (!dst || !src) {
assert(dst);
assert(src);
return nullptr;
}
// 正向拷贝
if (dst <= src) {
size_t head_bytes = (sizeof(uintptr_t) - (reinterpret_cast<uintptr_t>(dst) % sizeof(uintptr_t))) % sizeof(uintptr_t);
n -= head_bytes;
auto* slow_dst_ptr = reinterpret_cast<uint8_t*>(dst);
auto* slow_src_ptr = reinterpret_cast<const uint8_t*>(src);
while (head_bytes--) {
*slow_dst_ptr = *slow_src_ptr;
++slow_dst_ptr, ++slow_src_ptr;
}
size_t fast_steps = n / sizeof(uintptr_t);
if (reinterpret_cast<uintptr_t>(slow_src_ptr) % sizeof(uintptr_t) == 0 && fast_steps > 0) {
n %= sizeof(uintptr_t);
auto* fast_dst_ptr = reinterpret_cast<uintptr_t*>(slow_dst_ptr);
auto* fast_src_ptr = reinterpret_cast<const uintptr_t*>(slow_src_ptr);
assert((uintptr_t)fast_dst_ptr % sizeof(uintptr_t) == 0);
assert((uintptr_t)fast_src_ptr % sizeof(uintptr_t) == 0);
while (fast_steps--) {
*fast_dst_ptr = *fast_src_ptr;
++fast_dst_ptr, ++fast_src_ptr;
}
slow_dst_ptr = reinterpret_cast<uint8_t*>(fast_dst_ptr);
slow_src_ptr = reinterpret_cast<const uint8_t*>(fast_src_ptr);
}
while (n--) {
*slow_dst_ptr = *slow_src_ptr;
++slow_dst_ptr, ++slow_src_ptr;
}
}
// 反向拷贝
else {
size_t tail_bytes = (reinterpret_cast<uintptr_t>(dst) + n) % sizeof(uintptr_t);
auto* slow_dst_ptr = reinterpret_cast<uint8_t*>(dst) + n - 1;
auto* slow_src_ptr = reinterpret_cast<const uint8_t*>(src) + n - 1;
n -= tail_bytes;
while (tail_bytes--) {
*slow_dst_ptr = *slow_src_ptr;
--slow_dst_ptr, --slow_src_ptr;
}
size_t fast_steps = n / sizeof(uintptr_t);
if ((reinterpret_cast<uintptr_t>(slow_src_ptr) + 1) % sizeof(uintptr_t) == 0 && fast_steps > 0) {
n %= sizeof(uintptr_t);
auto* fast_dst_ptr = reinterpret_cast<uintptr_t*>(slow_dst_ptr - 7);
auto* fast_src_ptr = reinterpret_cast<const uintptr_t*>(slow_src_ptr - 7);
assert((uintptr_t)fast_dst_ptr % sizeof(uintptr_t) == 0);
assert((uintptr_t)fast_src_ptr % sizeof(uintptr_t) == 0);
while (fast_steps--) {
*fast_dst_ptr = *fast_src_ptr;
--fast_dst_ptr, --fast_src_ptr;
}
slow_dst_ptr = reinterpret_cast<uint8_t*>(fast_dst_ptr) + 7;
slow_src_ptr = reinterpret_cast<const uint8_t*>(fast_src_ptr) + 7;
}
while (n--) {
*slow_dst_ptr = *slow_src_ptr;
--slow_dst_ptr, --slow_src_ptr;
}
}
return dst;
}
void TestAnswer() {
char buffer1[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
char buffer2[30] = { 0 };
memmove(buffer2, buffer1, 15);
std::cout << buffer2 << std::endl;
memmove(buffer1 + 2, buffer1, 15);
std::cout << buffer1 << std::endl;
char buffer3[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
memmove(buffer3 + 5, buffer3 + 2, 15);
std::cout << buffer3 << std::endl;
char buffer4[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
memmove(buffer4 + 2, buffer4 + 5, 15);
std::cout << buffer4 << std::endl;
}
void TestMyFunc() {
char buffer1[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
char buffer2[30] = {0};
MemoryMove(buffer2, buffer1, 15);
std::cout << buffer2 << std::endl;
MemoryMove(buffer1 + 2, buffer1, 15);
std::cout << buffer1 << std::endl;
char buffer3[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
MemoryMove(buffer3 + 5, buffer3 + 2, 15);
std::cout << buffer3 << std::endl;
char buffer4[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
MemoryMove(buffer4 + 2, buffer4 + 5, 15);
std::cout << buffer4 << std::endl;
}
int main() {
TestAnswer();
std::cout << "---------------------" << std::endl;
TestMyFunc();
}
面试官考察意图分析
第1层:基本功与严谨性
- 考察点:处理内存重叠。
- 面试官心理:"这个人懂不懂 memmove 和 memcpy 的核心区别?代码风格是否严谨?能不能考虑到 dst > src 且重叠这种最关键的边界情况?"
- 预期表现:候选人能够使用 if (dst < src) 来区分拷贝方向。这是"合格"的基准线。
第2层:性能优化意识
- 考察点:是否知道逐字节拷贝效率低,并尝试进行块拷贝。
- 面试官心理:"OK,他写对了。但题目要求是'尽可能高性能',他有没有计算机组成原理方面的知识背景?知不知道可以按CPU字长(也就是CPU通用寄存器的最大宽度)来拷贝?"
- 预期表现:候选人使用 uintptr_t* 类型的指针来进行8字节的块拷贝,这让候选人从"合格"提升到了"良好"。
第3层:底层知识深度
-
考察点:对内存对齐(Alignment)的理解。
-
面试官心理:"他想到了用 uintptr_t*,很好。但他知不知道这里面有个巨大的陷阱?他会不会直接 reinterpret_cast 然后就以为万事大吉了?这能区分出他是死记硬背还是真正理解了内存对齐的概念。"
-
预期表现 :候选人在给出逐字节拷贝版本的代码实现后,能主动说出:"我这里的实现有一个简化,它假设了源和目的地址都是8字节对齐的。在实际情况中,如果地址不对齐,直接这样转换并访问,在某些CPU架构上会直接导致程序崩溃,在另一些架构(如x86)上也会因为CPU的额外处理而导致性能严重下降,违背了优化的初衷。"
- 仅仅说出这段话,即使你的代码没有实现对齐处理,也足以让你在面试官心中达到"优秀"的水平。这证明了你的知识深度和对风险的预判能力。
第44层:知识广度与视野
-
考察点:是否了解现代计算机体系结构中真正的高性能方案。
-
面试官心理:"非常好,他连对齐的坑都知道了。那他知不知道在现实中,C/C++底层的标准库是怎么做到极致性能的?"
-
预期表现 :在讨论完对齐问题后,候选人能进一步补充:"其实,要做到真正的极致性能,手写C++循环已经不是最佳方案了。现代编译器在开启优化后,很多时候能比手写的循环做得更好。而标准库(如Glibc)中的 memmove 实现,通常会利用CPU的SIMD(单指令多数据流)指令集 ,比如x86平台上的 SSE 或 AVX 指令,它们可以一次性操作128位(16字节)、256位(32字节)甚至512位的数据。这些实现是高度优化的、平台相关的,并且通常是用汇编或者编译器内置函数(Intrinsics)来编写的,这才是工业界的标准实现。"
- 说出这段话,就展示了你的知识广度与技术视野。这会让面试官认为你不仅基础扎实,而且对教科书之外的拓展知识有所了解。即使说不出具体的指令名称,也是巨大的加分项。