Подтвердить что ты не робот

Как увеличить производительность memcpy

Резюме:

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

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

Как часть приложения для захвата данных (с использованием некоторых специализированных аппаратных средств), мне нужно скопировать около 3 ГБ/сек из временных буферов в основную память. Чтобы получить данные, я предоставляю драйверу оборудования ряд буферов (по 2 МБ каждый). Аппаратные данные DMAs для каждого буфера, а затем уведомляет мою программу, когда каждый буфер заполнен. Моя программа опустошает буфер (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 С++ 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;
}

EDIT: если у вас есть дополнительные пять минут и вы хотите внести свой вклад, можете ли вы запустить вышеуказанный код на своем компьютере и опубликовать свое время в качестве комментария?

4b9b3361

Ответ 1

Я нашел способ увеличить скорость в этой ситуации. Я написал многопоточную версию 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;
}

Ответ 2

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

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

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

Ответ 3

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

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

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

  • Процессор - перемещаемые данные имеют долгий путь: RAM → L2 Cache → L1 Cache → CPU → L1 → L2 → RAM. Возможно, даже есть кеш L3. Если вы хотите задействовать CPU, вы действительно хотите загрузить L2 во время копирования L1. К сожалению, современные процессоры могут работать через блок кэша L1 быстрее, чем время загрузки L1. Процессор имеет контроллер памяти, который очень помогает в этих случаях, когда потоковые данные в CPU последовательно, но у вас все еще будут проблемы.

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

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

вместо:

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

Или получите более быструю ОЗУ!

EDIT: Еще одна опция - обрабатывать данные между источником данных и ПК - не могли бы вы вообще разместить DSP/FPGA? Пользовательское оборудование всегда будет быстрее, чем CPU общего назначения.

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

Ответ 4

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

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

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

Ответ 5

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

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

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

Ответ 6

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

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

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

Ответ 7

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

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

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

Ответ 8

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