Memcpy против Memmove - отладка против выпуска

Я получил очень странное поведение для своего многопоточного приложения 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);
}

2 ответа

Решение

Следующий ответ действителен ТОЛЬКО для 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. Я подтвердил это, взяв реализацию CRT из исходного кода, включенного в Visual Studio, и сделав my_memcpy

В качестве способа обеспечения того, чтобы кэш был теплым во время всего этого, я предварительно инициализировал все data но результаты говорили:

Разогрев занял 43мс
my_memcpy up заняло 862мс
memmove up занял 676мс
memcpy up занял 1329мс

Так почему же 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

По словам Бозона, в комментариях все зависит от архитектуры. На моем процессоре rep movsb быстрее, но на старых процессорах реализация SSE или AVX потенциально может быть быстрее. Это в соответствии с Руководством по оптимизации Intel. Для невыровненных данных как rep movsb может испытывать до 25% штрафа на старом оборудовании. Тем не менее, как представляется, для подавляющего большинства случаев и архитектур rep movsb в среднем превзойдет реализацию SSE или AVX.

Идея: позвонить memmove, поскольку это самый быстрый для вашего случая.

Другие вопросы по тегам