【问题标题】:Memcpy vs Memmove - Debug vs ReleaseMemcpy 与 Memmove - 调试与发布
【发布时间】:2015-01-13 09:23:02
【问题描述】:

我的 x64 多线程应用程序出现了非常奇怪的行为。 调试模式的执行时间比发布模式快。

我分解了问题并发现了问题: 调试方式优化(!注意优化已关闭!)memcpy 到 memmove,它执行得更快。发布模式仍然使用 memcpy (!note 优化开启)。

这个问题会在发布模式下减慢我的多线程应用程序。 :(

有人知道吗?

#include <time.h>
#include <iostream>

#define T_SIZE 1024*1024*2

int main()
{
    clock_t start, end;

    char data[T_SIZE];
    char store[100][T_SIZE];

    start = clock();
    for (int i = 0; i < 4000; i++) {
        memcpy(store[i % 100], data, T_SIZE);
    }
    // Debug > Release Time 1040 < 1620
    printf("memcpy: %d\n", clock() - start);

    start = clock();
    for (int i = 0; i < 4000; i++) {
        memmove(store[i % 100], data, T_SIZE);
    }
    // Debug > Release Time 1040 > 923
    printf("memmove: %d\n", clock() - start);
}

【问题讨论】:

  • 这是未定义的行为,您尚未初始化数组。你为什么还要使用memcpymemmove
  • 使用_aligned_malloc分配内存性能是否也不同? (对齐值大)
  • 许多编译器会在 200 MB 堆栈帧上阻塞。
  • @user657267:复制未初始化的char 值的定义非常好。 int 会有所不同,但只是形式上的。
  • 你必须有一个相当旧的 VS 版本,它仍然在 Release 构建中省略了 _chkstk() 探针。它隐藏了提交和映射内存页面的成本,_chkstk() 使这发生在您启动计时器之前。在真正的挂钟时间里,它并没有更快。要比较苹果和橙子,您需要将代码移动到单独的函数中,并使用 __declspec(noinline) 对其进行属性。

标签: c++ visual-studio debugging c++11 release


【解决方案1】:

以下答案对 VS2013 有效

我们这里的内容实际上比 memcpymemmove 更奇怪。这是内在优化实际上减慢速度的一个例子。问题源于 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 实现。

【讨论】:

  • 非常感谢您的解释。我很确定,这个编译器内在问题也会影响其他库。在我的项目中,我使用的是 boost,只是将 memcopy 更改为 memmove 并不会像调试版本那样加快速度。我将您的答案标记为正确,因为它是正确的。在编译库以避免这种减速时覆盖 memcpy 是否可能会节省?
  • @Viatorus 不,memcopy 是保留名称,不应混淆。我建议使用std::copy 而不是其中任何一个,因为它允许运行时选择更好的(VC++ 运行时使用memmove)并且是类型安全的。
  • 你在哪个 CPU 上测试过这个?
  • @Zboson 英特尔酷睿 i7 4600U。使用未对齐指令的对齐加载在 SSE 过去 nahalem 中没有任何惩罚,因此 __declspec(align(16)) 应该可以工作,但它没有。
  • @PeterCordes 绝对,如果 CPU 可以判断指令的数据依赖性是独立的(因此可以同时预取和执行),它们可以竞争。在这种情况下,CPU 似乎足够聪明,可以检测到缺乏依赖性,并使用寄存器阴影来加快速度,这反过来很可能会混淆看到非线性访问的预取器。
【解决方案2】:

想法:致电memmove,因为它对您来说是最快的。

【讨论】:

  • 抱歉,这不是解决方案。因为这个问题不仅仅存在于memcopy。如果堆栈大于 200mb,这只是一个工作示例。
  • @Viatorus:如果您想回答不同的问题,请务必发布该问题,不要在回答后细化或概括或以其他方式从根本上改变您的问题已发布。
  • 为什么memmove 会比memcpy 快​​?
  • 因为测试用例无效clock_t is not guaranteed to match wall clock所以整个事情都是无效的。如果他们要对这些东西进行基准测试,他们需要使用单调的高精度时钟。
  • @Mgetz 可能是,但不是。如果我增加迭代次数,它会在几秒钟内引起注意......
猜你喜欢
  • 2011-05-23
  • 1970-01-01
  • 2011-07-21
  • 2011-03-26
  • 1970-01-01
  • 2011-03-09
  • 2011-01-27
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多