以下答案对 VS2013 有效仅
我们这里的内容实际上比 memcpy 与 memmove 更奇怪。这是内在优化实际上减慢速度的一个例子。问题源于 VS2013 像这样内联 memcopy:
; 73 : memcpy(store[i % 100], data, sizeof(data));
mov eax, 1374389535 ; 51eb851fH
mul esi
shr edx, 5
imul eax, edx, 100 ; 00000064H
mov ecx, esi
sub ecx, eax
movsxd rcx, ecx
shl rcx, 21
add rcx, r14
mov rdx, r13
mov r8d, 16384 ; 00004000H
npad 12
$LL413@wmain:
movups xmm0, XMMWORD PTR [rdx]
movups XMMWORD PTR [rcx], xmm0
movups xmm1, XMMWORD PTR [rdx+16]
movups XMMWORD PTR [rcx+16], xmm1
movups xmm0, XMMWORD PTR [rdx+32]
movups XMMWORD PTR [rcx+32], xmm0
movups xmm1, XMMWORD PTR [rdx+48]
movups XMMWORD PTR [rcx+48], xmm1
movups xmm0, XMMWORD PTR [rdx+64]
movups XMMWORD PTR [rcx+64], xmm0
movups xmm1, XMMWORD PTR [rdx+80]
movups XMMWORD PTR [rcx+80], xmm1
movups xmm0, XMMWORD PTR [rdx+96]
movups XMMWORD PTR [rcx+96], xmm0
lea rcx, QWORD PTR [rcx+128]
movups xmm1, XMMWORD PTR [rdx+112]
movups XMMWORD PTR [rcx-16], xmm1
lea rdx, QWORD PTR [rdx+128]
dec r8
jne SHORT $LL413@wmain
问题在于我们正在执行未对齐的 SSE 加载和存储,这实际上比仅使用标准 C 代码要慢。我通过从 Visual Studio 中包含的源代码中获取 CRT 实现并制作 my_memcpy
来验证这一点
为了确保在所有这些过程中缓存是温暖的,我已经预先初始化了所有 data,但结果很明显:
预热耗时 43 毫秒
my_memcpy 占用了 862 毫秒
memmove up 耗时 676ms
memcpy up 耗时 1329ms
那么为什么memmove 更快?因为它不会尝试预先优化,因为它必须假设数据可以重叠。
对于那些好奇的人,这是我的完整代码:
#include <cstdlib>
#include <cstring>
#include <chrono>
#include <iostream>
#include <random>
#include <functional>
#include <limits>
namespace {
const auto t_size = 1024ULL * 1024ULL * 2ULL;
__declspec(align(16 )) char data[t_size];
__declspec(align(16 )) char store[100][t_size];
void * __cdecl my_memcpy(
void * dst,
const void * src,
size_t count
)
{
void * ret = dst;
/*
* copy from lower addresses to higher addresses
*/
while (count--) {
*(char *)dst = *(char *)src;
dst = (char *)dst + 1;
src = (char *)src + 1;
}
return(ret);
}
}
int wmain(int argc, wchar_t* argv[])
{
using namespace std::chrono;
std::mt19937 rd{ std::random_device()() };
std::uniform_int_distribution<short> dist(std::numeric_limits<char>::min(), std::numeric_limits<char>::max());
auto random = std::bind(dist, rd);
auto start = steady_clock::now();
// warms up the cache and initializes
for (int i = 0; i < t_size; ++i)
data[i] = static_cast<char>(random());
auto stop = steady_clock::now();
std::cout << "Warm up took " << duration_cast<milliseconds>(stop - start).count() << "ms\n";
start = steady_clock::now();
for (int i = 0; i < 4000; ++i)
my_memcpy(store[i % 100], data, sizeof(data));
stop = steady_clock::now();
std::cout << "my_memcpy took " << duration_cast<milliseconds>(stop - start).count() << "ms\n";
start = steady_clock::now();
for (int i = 0; i < 4000; ++i)
memmove(store[i % 100], data, sizeof(data));
stop = steady_clock::now();
std::cout << "memmove took " << duration_cast<milliseconds>(stop - start).count() << "ms\n";
start = steady_clock::now();
for (int i = 0; i < 4000; ++i)
memcpy(store[i % 100], data, sizeof(data));
stop = steady_clock::now();
std::cout << "memcpy took " << duration_cast<milliseconds>(stop - start).count() << "ms\n";
std::cin.ignore();
return 0;
}
更新
在调试时,我发现编译器确实检测到我从 CRT 复制的代码是 memcpy,但它会将其链接到 CRT 本身的非内在版本,该版本使用 rep movs 而不是大量上面的 SSE 循环。似乎问题仅在于内在版本。
更新 2
cmets 中的每个 Z 玻色子似乎都非常依赖于架构。在我的 CPU rep movsb 上更快,但在较旧的 CPU 上,SSE 或 AVX 实现有可能更快。这是根据Intel Optimization Manual。对于未对齐的数据,rep movsb 在旧硬件上可能会受到高达 25% 的损失。然而,话虽如此,对于绝大多数情况和架构,rep movsb 似乎平均会击败 SSE 或 AVX 实现。