Как повысить производительность memcpy - PullRequest
47 голосов
/ 23 ноября 2010

Резюме:

memcpy, по-видимому, не может передавать более 2 ГБ / с в моей системе в реальном или тестовом приложении. Что я могу сделать, чтобы получить более быстрые копии из памяти в память?

Полная информация:

Как часть приложения для сбора данных (с использованием некоторого специализированного оборудования) мне нужно скопировать около 3 ГБ / с из временных буферов в основную память. Для сбора данных я предоставляю драйверу оборудования серию буферов (по 2 МБ каждый). Аппаратные DMA передают данные в каждый буфер, а затем уведомляют мою программу о заполнении каждого буфера. Моя программа очищает буфер (memcpy в другой, больший блок оперативной памяти) и повторно помещает обработанный буфер в карту для повторного заполнения. У меня проблемы с memcpy, перемещая данные достаточно быстро. Кажется, что копирование из памяти в память должно быть достаточно быстрым, чтобы поддерживать 3 ГБ / с на оборудовании, на котором я работаю. Lavalys EVEREST дает мне результат теста производительности копирования памяти 9337 МБ / с, но я не могу приблизиться к этим скоростям с помощью memcpy, даже в простой тестовой программе.

Я выделил проблему производительности, добавив / удалив вызов memcpy внутри кода обработки буфера. Без memcpy я могу работать с полной скоростью передачи данных - около 3 ГБ / с. С включенной memcpy я ограничен до 550 Мб / с (используя текущий компилятор).

Чтобы протестировать memcpy в моей системе, я написал отдельную тестовую программу, которая просто вызывает memcpy для некоторых блоков данных. (Я разместил код ниже). Я запустил его как в компиляторе / IDE, который я использую (National Instruments CVI), так и в Visual Studio 2010. Хотя в настоящее время я не использую Visual Studio, я готов сделать переключатель, если он даст необходимую производительность. Однако, прежде чем вслепую перейти, я хотел убедиться, что это решит мои проблемы с производительностью memcpy.

Visual C ++ 2010: 1900 МБ / с

NI CVI 2009: 550 МБ / с

Хотя я не удивлен, что CVI значительно медленнее, чем Visual Studio, я удивлен, что производительность memcpy такая низкая. Хотя я не уверен, что это прямо сопоставимо, это намного ниже, чем пропускная способность EVEREST. Хотя мне не нужен такой уровень производительности, требуется минимум 3 ГБ / с. Конечно, реализация стандартной библиотеки не может быть намного хуже, чем то, что использует EVEREST! ​​

Что, если что, я могу сделать, чтобы memcpy быстрее работал в этой ситуации?


Детали оборудования: AMD Magny Cours - восьмеричное ядро ​​4x 128 ГБ DDR3 Windows Server 2003 Enterprise X64

Тестовая программа:

#include <windows.h>
#include <stdio.h>

const size_t NUM_ELEMENTS = 2*1024 * 1024;
const size_t ITERATIONS = 10000;

int main (int argc, char *argv[])
{
    LARGE_INTEGER start, stop, frequency;

    QueryPerformanceFrequency(&frequency);

    unsigned short * src = (unsigned short *) malloc(sizeof(unsigned short) * NUM_ELEMENTS);
    unsigned short * dest = (unsigned short *) malloc(sizeof(unsigned short) * NUM_ELEMENTS);

    for(int ctr = 0; ctr < NUM_ELEMENTS; ctr++)
    {
        src[ctr] = rand();
    }

    QueryPerformanceCounter(&start);

    for(int iter = 0; iter < ITERATIONS; iter++)
        memcpy(dest, src, NUM_ELEMENTS * sizeof(unsigned short));

    QueryPerformanceCounter(&stop);

    __int64 duration = stop.QuadPart - start.QuadPart;

    double duration_d = (double)duration / (double) frequency.QuadPart;

    double bytes_sec = (ITERATIONS * (NUM_ELEMENTS/1024/1024) * sizeof(unsigned short)) / duration_d;

    printf("Duration: %.5lfs for %d iterations, %.3lfMB/sec\n", duration_d, ITERATIONS, bytes_sec);

    free(src);
    free(dest);

    getchar();

    return 0;
}

РЕДАКТИРОВАТЬ: Если у вас есть дополнительные пять минут и вы хотите внести свой вклад, вы можете запустить приведенный выше код на своем компьютере и опубликовать свое время в качестве комментария?

Ответы [ 8 ]

32 голосов
/ 24 ноября 2010

Я нашел способ увеличить скорость в этой ситуации.Я написал многопоточную версию memcpy, разделив область для копирования между потоками.Вот некоторые цифры масштабирования производительности для установленного размера блока с использованием того же временного кода, который был найден выше.Я понятия не имел, что производительность, особенно для этого небольшого размера блока, будет масштабироваться до такого количества потоков.Я подозреваю, что это как-то связано с большим количеством контроллеров памяти (16) на этой машине.

Performance (10000x 4MB block memcpy):

 1 thread :  1826 MB/sec
 2 threads:  3118 MB/sec
 3 threads:  4121 MB/sec
 4 threads: 10020 MB/sec
 5 threads: 12848 MB/sec
 6 threads: 14340 MB/sec
 8 threads: 17892 MB/sec
10 threads: 21781 MB/sec
12 threads: 25721 MB/sec
14 threads: 25318 MB/sec
16 threads: 19965 MB/sec
24 threads: 13158 MB/sec
32 threads: 12497 MB/sec

Я не понимаю огромный скачок производительности между 3 и 4 потоками.Что может вызвать скачок, подобный этому?

Я включил код memcpy, который я написал ниже для других, которые могут столкнуться с этой же проблемой.Обратите внимание, что в этом коде нет проверки ошибок, возможно, ее необходимо добавить для вашего приложения.

#define NUM_CPY_THREADS 4

HANDLE hCopyThreads[NUM_CPY_THREADS] = {0};
HANDLE hCopyStartSemaphores[NUM_CPY_THREADS] = {0};
HANDLE hCopyStopSemaphores[NUM_CPY_THREADS] = {0};
typedef struct
{
    int ct;
    void * src, * dest;
    size_t size;
} mt_cpy_t;

mt_cpy_t mtParamters[NUM_CPY_THREADS] = {0};

DWORD WINAPI thread_copy_proc(LPVOID param)
{
    mt_cpy_t * p = (mt_cpy_t * ) param;

    while(1)
    {
        WaitForSingleObject(hCopyStartSemaphores[p->ct], INFINITE);
        memcpy(p->dest, p->src, p->size);
        ReleaseSemaphore(hCopyStopSemaphores[p->ct], 1, NULL);
    }

    return 0;
}

int startCopyThreads()
{
    for(int ctr = 0; ctr < NUM_CPY_THREADS; ctr++)
    {
        hCopyStartSemaphores[ctr] = CreateSemaphore(NULL, 0, 1, NULL);
        hCopyStopSemaphores[ctr] = CreateSemaphore(NULL, 0, 1, NULL);
        mtParamters[ctr].ct = ctr;
        hCopyThreads[ctr] = CreateThread(0, 0, thread_copy_proc, &mtParamters[ctr], 0, NULL); 
    }

    return 0;
}

void * mt_memcpy(void * dest, void * src, size_t bytes)
{
    //set up parameters
    for(int ctr = 0; ctr < NUM_CPY_THREADS; ctr++)
    {
        mtParamters[ctr].dest = (char *) dest + ctr * bytes / NUM_CPY_THREADS;
        mtParamters[ctr].src = (char *) src + ctr * bytes / NUM_CPY_THREADS;
        mtParamters[ctr].size = (ctr + 1) * bytes / NUM_CPY_THREADS - ctr * bytes / NUM_CPY_THREADS;
    }

    //release semaphores to start computation
    for(int ctr = 0; ctr < NUM_CPY_THREADS; ctr++)
        ReleaseSemaphore(hCopyStartSemaphores[ctr], 1, NULL);

    //wait for all threads to finish
    WaitForMultipleObjects(NUM_CPY_THREADS, hCopyStopSemaphores, TRUE, INFINITE);

    return dest;
}

int stopCopyThreads()
{
    for(int ctr = 0; ctr < NUM_CPY_THREADS; ctr++)
    {
        TerminateThread(hCopyThreads[ctr], 0);
        CloseHandle(hCopyStartSemaphores[ctr]);
        CloseHandle(hCopyStopSemaphores[ctr]);
    }
    return 0;
}
9 голосов
/ 23 ноября 2010

Я не уверен, выполняется ли это во время выполнения или нужно ли это делать во время компиляции, но у вас должны быть включены SSE или аналогичные расширения, поскольку векторный модуль часто может записывать 128 бит в память по сравнению с 64 битами дляCPU.

Попробуйте эту реализацию .

Да, и убедитесь, что и источник и пункт назначения выровненыдо 128 бит.Если ваш источник и пункт назначения не выровнены относительно друг друга, ваш memcpy () должен будет сделать некоторую серьезную магию.:)

5 голосов
/ 24 ноября 2010

Следует помнить, что на ваш процесс (и, следовательно, на производительность memcpy()) влияет планирование задач ОС. Трудно сказать, насколько важен этот фактор в ваших временных параметрах. трудно контролировать. Операция прямого доступа к памяти устройства не подчиняется этому, так как она не запускается на процессоре после его запуска. Поскольку ваше приложение является приложением реального времени, вы можете поэкспериментировать с настройками приоритетов процессов / потоков Windows, если вы этого еще не сделали. Просто имейте в виду, что вы должны быть осторожны с этим, потому что это может оказать действительно негативное влияние на другие процессы (и взаимодействие с пользователем на машине).

Еще одна вещь, которую нужно иметь в виду, заключается в том, что виртуализация памяти операционной системы может оказать здесь влияние - если страницы памяти, на которые вы копируете, фактически не поддерживаются страницами физической памяти, операция memcpy() приведет к сбою ОС чтобы получить эту физическую поддержку на месте. Ваши страницы DMA, вероятно, будут заблокированы в физической памяти (так как они должны быть для операции DMA), поэтому память источника до memcpy(), вероятно, не является проблемой в этом отношении. Вы могли бы рассмотреть возможность использования API Win32 VirtualAlloc(), чтобы гарантировать, что ваша целевая память для memcpy() выделена (я думаю, VirtualAlloc() является правильным API для этого, но, возможно, есть лучший, который я забыл - это я уже давно не хотел ничего подобного).

Наконец, посмотрите, можете ли вы использовать технику, описанную Skizz , чтобы вообще избежать memcpy() - это ваш лучший выбор, если позволят ресурсы.

5 голосов
/ 24 ноября 2010

У вас есть несколько препятствий для получения требуемой производительности памяти:

  1. Пропускная способность - существует ограничение скорости передачи данных из памяти в ЦП и обратно. Согласно этой статье в Википедии , память DDR3 266 МГц имеет верхний предел около 17 ГБ / с. Теперь, с помощью memcpy, вам нужно уменьшить это значение вдвое, чтобы получить максимальную скорость передачи, поскольку данные читаются, а затем записываются. Исходя из результатов тестов, похоже, что у вас не самая быстрая оперативная память в вашей системе. Если вы можете себе это позволить, обновите материнскую плату / ОЗУ (и это будет недешево, в настоящее время в Великобритании у оверклокеров есть 3x4GB PC16000 за 400 фунтов стерлингов)

  2. ОС - Windows - это многозадачная ОС с вытеснением, поэтому время от времени ваш процесс приостанавливается, чтобы позволить другим процессам взглянуть и что-то сделать. Это заглушит ваши кеши и остановит передачу. В худшем случае весь ваш процесс может быть кэширован на диск!

  3. ЦП - перемещаемым данным предстоит пройти долгий путь: ОЗУ -> Кэш L2 -> Кэш L1 -> ЦП -> L1 -> L2 -> ОЗУ. Там даже может быть кэш L3. Если вы хотите задействовать процессор, вы действительно хотите загружать L2 при копировании L1. К сожалению, современные процессоры могут проходить через блок кэша L1 быстрее, чем время, необходимое для загрузки L1. ЦП имеет контроллер памяти, который очень помогает в тех случаях, когда ваши потоковые данные поступают в ЦП последовательно, но у вас все еще будут проблемы.

Конечно, более быстрый способ сделать что-то - это не делать этого. Можно ли записывать захваченные данные в любое место в ОЗУ или использовать буфер в фиксированном месте Если вы можете написать это где-нибудь, то вам не нужен memcpy вообще. Если это исправлено, не могли бы вы обработать данные на месте и использовать систему с двойным буфером? То есть начните сбор данных, а когда он наполовину заполнен, начните обрабатывать первую половину данных. Когда буфер заполнится, начните записывать захваченные данные в начало и обработайте вторую половину. Это требует, чтобы алгоритм мог обрабатывать данные быстрее, чем карта захвата их производит. Также предполагается, что данные отбрасываются после обработки. По сути, это memcpy с преобразованием как частью процесса копирования, поэтому у вас есть:

load -> transform -> save
\--/                 \--/
 capture card        RAM
   buffer

вместо:

load -> save -> load -> transform -> save
\-----------/
memcpy from
capture card
buffer to RAM

Или получить быстрее оперативной памяти!

РЕДАКТИРОВАТЬ: Другой вариант заключается в обработке данных между источником данных и ПК - вы могли бы вообще поставить DSP / FPGA? Пользовательское оборудование всегда будет быстрее, чем процессор общего назначения.

Еще одна мысль: я уже давно занимался какими-то высокопроизводительными графическими работами, но не могли бы вы отправить данные в графическую карту, а затем снова в нее? Вы даже можете воспользоваться CUDA, чтобы выполнить некоторую обработку. Это вообще вывело бы ЦП из цикла передачи памяти.

2 голосов
/ 24 ноября 2010

Вы можете написать лучшую реализацию memcpy, используя регистры SSE2.Версия в VC2010 уже делает это.Так что вопрос больше, если вы передаете ему выровненную память.

Возможно, вы можете сделать лучше, чем версия VC 2010, но для этого нужно некоторое понимание того, как это сделать.

PS: Вы можете передать буфер в программу пользовательского режима вперевернутый вызов, чтобы вообще предотвратить копирование.

2 голосов
/ 23 ноября 2010

Возможно, вы можете объяснить подробнее, как вы обрабатываете большую область памяти?

Возможно ли в вашем приложении просто передать владение буфером, а не скопировать его?Это полностью устранит проблему.

Или вы используете memcpy не только для копирования?Возможно, вы используете большую область памяти для создания последовательного потока данных из того, что вы захватили?Особенно, если вы обрабатываете по одному персонажу за раз, вы можете встретиться на полпути.Например, может быть возможно адаптировать ваш код обработки для размещения потока, представленного как «массив буферов», а не как «непрерывная область памяти».

2 голосов
/ 23 ноября 2010

Прежде всего, вам необходимо убедиться, что память выровнена по границе 16 байт, иначе вы получите штрафы. Это самая важная вещь.

Если вам не нужно стандартно-совместимое решение, вы можете проверить, улучшается ли что-либо, используя какое-то специфическое для компилятора расширение, например memcpy64 (уточните в документации по вашему компилятору, есть ли что-то доступное). Фактом является то, что memcpy должен иметь возможность работать с однобайтовой копией, но перемещение 4 или 8 байтов за раз происходит намного быстрее, если у вас нет этого ограничения.

Опять же, вы можете написать встроенный ассемблерный код?

1 голос
/ 24 ноября 2010

Один источник, который я бы порекомендовал вам прочитать - это функция MPlayer fast_memcpy. Также обратите внимание на ожидаемые модели использования и обратите внимание, что современные процессоры имеют специальные инструкции по хранению, которые позволяют вам сообщить процессору, нужно ли вам считывать данные, которые вы пишете. Использование инструкций, указывающих, что вы не будете читать данные (и, следовательно, их не нужно кэшировать), может быть огромным выигрышем для больших операций memcpy.

...