Почему memcpy () и memmove () быстрее, чем приращение указателя? - PullRequest
89 голосов
/ 15 октября 2011

Я копирую N байтов из pSrc в pDest.Это можно сделать за один цикл:

for (int i = 0; i < N; i++)
    *pDest++ = *pSrc++

Почему это медленнее, чем memcpy или memmove?Какие уловки они используют, чтобы ускорить его?

Ответы [ 9 ]

113 голосов
/ 15 октября 2011

Поскольку memcpy использует указатели слов вместо байтовых указателей, также реализации memcpy часто пишутся с инструкциями SIMD , которые позволяют перетасовывать 128 битов за раз.

Инструкции SIMDинструкции по сборке, которые могут выполнять одну и ту же операцию для каждого элемента в векторе длиной до 16 байтов.Сюда входят инструкции по загрузке и хранению.

78 голосов
/ 15 октября 2011

Процедуры копирования памяти могут быть намного более сложными и быстрыми, чем простая копия памяти с помощью указателей, таких как:

void simple_memory_copy(void* dst, void* src, unsigned int bytes)
{
  unsigned char* b_dst = (unsigned char*)dst;
  unsigned char* b_src = (unsigned char*)src;
  for (int i = 0; i < bytes; ++i)
    *b_dst++ = *b_src++;
}

Улучшения

Первое улучшение, которое можно сделать, - это выровнять один из указателей по границе слова (под словом я имею в виду собственный целочисленный размер, обычно 32 бита / 4 байта, но может быть 64 бита / 8 байт на более новой архитектуре) и использовать размерные инструкции перемещения / копирования. Это требует использования байта для копирования байтов, пока указатель не будет выровнен.

void aligned_memory_copy(void* dst, void* src, unsigned int bytes)
{
  unsigned char* b_dst = (unsigned char*)dst;
  unsigned char* b_src = (unsigned char*)src;

  // Copy bytes to align source pointer
  while ((b_src & 0x3) != 0)
  {
    *b_dst++ = *b_src++;
    bytes--;
  }

  unsigned int* w_dst = (unsigned int*)b_dst;
  unsigned int* w_src = (unsigned int*)b_src;
  while (bytes >= 4)
  {
    *w_dst++ = *w_src++;
    bytes -= 4;
  }

  // Copy trailing bytes
  if (bytes > 0)
  {
    b_dst = (unsigned char*)w_dst;
    b_src = (unsigned char*)w_src;
    while (bytes > 0)
    {
      *b_dst++ = *b_src++;
      bytes--;
    }
  }
}

Различные архитектуры будут работать по-разному в зависимости от того, правильно ли выровнены указатель источника или назначения. Например, на процессоре XScale я получил лучшую производительность, совместив указатель назначения, а не указатель источника.

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

На этом этапе код заканчивается написанием на ассемблере, а не на C (или C ++), поскольку вам необходимо вручную разместить инструкции загрузки и сохранения, чтобы получить максимальную выгоду от сокрытия задержки и пропускной способности.

Обычно всю строку данных кэша следует копировать за одну итерацию развернутого цикла.

Что подводит меня к следующему улучшению, добавляя предварительную выборку. Это специальные инструкции, которые говорят кеш-системе процессора загружать определенные части памяти в кеш. Поскольку существует задержка между выполнением инструкции и заполнением строки кэша, инструкции должны быть размещены таким образом, чтобы данные были доступны, когда они должны быть скопированы, и не раньше / позже.

Это означает размещение инструкций предварительной выборки в начале функции, а также внутри основного цикла копирования. С инструкциями предварительной выборки в середине цикла копирования, извлекающими данные, которые будут скопированы за несколько итераций.

Не помню, но может быть также полезно предварительно выбрать адреса назначения, а также адреса источника.

Факторы

Основные факторы, влияющие на скорость копирования памяти:

  • Задержка между процессором, его кэшами и основной памятью.
  • Размер и структура строк кэша процессора.
  • Инструкции перемещения / копирования памяти процессора (задержка, пропускная способность, размер регистра и т. Д.).

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

18 голосов
/ 15 октября 2011

memcpy может копировать более одного байта одновременно, в зависимости от архитектуры компьютера.Большинство современных компьютеров могут работать с 32 битами или более в одной инструкции процессора.

Из один пример реализации :

    00026          * For speedy copying, optimize the common case where both pointers
    00027          * and the length are word-aligned, and copy word-at-a-time instead
    00028          * of byte-at-a-time. Otherwise, copy by bytes.
7 голосов
/ 15 октября 2011

Вы можете реализовать memcpy(), используя любой из следующих методов, некоторые из которых зависят от вашей архитектуры для повышения производительности, и все они будут намного быстрее, чем ваш код:

  1. Используйте более крупные единицы, такие как 32-битные слова вместо байтов. Вы также можете (или, возможно, должны) иметь дело с выравниванием здесь. Вы не можете читать / записывать 32-разрядное слово в нечетную область памяти, например, на некоторых платформах, а на других платформах вы платите огромные потери производительности. Чтобы исправить это, адрес должен делиться на единицу, делимую на 4. Вы можете взять это значение до 64 бит для 64-битных процессоров или даже выше, используя инструкции SIMD (одна команда, несколько данных) (* 1008) * MMX , SSE и т. Д.)

  2. Вы можете использовать специальные инструкции процессора, которые ваш компилятор может не оптимизировать из C. Например, на 80386 вы можете использовать инструкцию префикса «rep» + инструкция «movsb», чтобы переместить продиктованные N байтов поместив N в счетный регистр. Хорошие компиляторы просто сделают это для вас, но вы можете быть на платформе, в которой нет хорошего компилятора. Обратите внимание, что этот пример, как правило, является плохой демонстрацией скорости, но в сочетании с выравниванием + более крупными инструкциями юнитов он может быть быстрее, чем все остальное на определенных процессорах.

  3. Развертывание циклов - ветки могут быть довольно дорогими на некоторых процессорах, поэтому развертывание циклов может уменьшить количество ветвей. Это также хороший метод для объединения с инструкциями SIMD и очень крупными единицами измерения.

Например, http://www.agner.org/optimize/#asmlib имеет реализацию memcpy, которая превосходит большинство (очень незначительное количество). Если вы прочитаете исходный код, он будет полон тонн встроенного ассемблерного кода, который использует все три вышеупомянутых метода, выбирая, какой из этих методов зависит от того, на каком процессоре вы работаете.

Обратите внимание, что существуют похожие оптимизации для поиска байтов в буфере. strchr() и друзья часто будут быстрее, чем ваш эквивалент. Это особенно верно для .NET и Java . Например, в .NET встроенный String.IndexOf() намного быстрее, чем даже поиск строки Бойера-Мура , потому что он использует описанные выше методы оптимизации.

5 голосов
/ 16 октября 2011

Краткий ответ:

  • заполнение кэша
  • передача по размеру слова вместо байтов, где это возможно
  • SIMD magic
4 голосов
/ 15 октября 2011

Я не знаю, действительно ли оно используется в реальных реализациях memcpy, но я думаю, что Устройство Даффа заслуживает упоминания здесь.

От Википедия :

send(to, from, count)
register short *to, *from;
register count;
{
        register n = (count + 7) / 8;
        switch(count % 8) {
        case 0:      do {     *to = *from++;
        case 7:              *to = *from++;
        case 6:              *to = *from++;
        case 5:              *to = *from++;
        case 4:              *to = *from++;
        case 3:              *to = *from++;
        case 2:              *to = *from++;
        case 1:              *to = *from++;
                } while(--n > 0);
        }
}

Обратите внимание, что приведенное выше не является memcpy, поскольку оно намеренно не увеличивает указатель to.В нем реализована немного другая операция: запись в регистр с отображением в памяти.Подробности смотрите в статье Википедии.

3 голосов
/ 15 октября 2011

Как и другие говорят, что копии memcpy больше 1-байтовых кусков. Копирование кусками размером в слово происходит намного быстрее Тем не менее, большинство реализаций делают шаг вперед и запускают несколько инструкций MOV (word) перед циклом. Преимущество копирования, скажем, блоков из 8 слов в цикле состоит в том, что сам цикл является дорогостоящим. Этот метод уменьшает количество условных ветвлений в 8 раз, оптимизируя копирование для гигантских блоков.

2 голосов
/ 15 октября 2011

Ответы отличные, но если вы все еще хотите внедрить fast memcpy самостоятельно, есть интересное сообщение в блоге о fast memcpy, Fast memcpy в C .

void *memcpy(void* dest, const void* src, size_t count)
{
    char* dst8 = (char*)dest;
    char* src8 = (char*)src;

    if (count & 1) {
        dst8[0] = src8[0];
        dst8 += 1;
        src8 += 1;
    }

    count /= 2;
    while (count--) {
        dst8[0] = src8[0];
        dst8[1] = src8[1];

        dst8 += 2;
        src8 += 2;
    }
    return dest;
}

Даже лучше оптимизировать доступ к памяти.

1 голос
/ 15 октября 2011

Потому что, как и многие библиотечные процедуры, он был оптимизирован для архитектуры, на которой вы работаете.Другие опубликовали различные методы, которые могут быть использованы.

Если есть выбор, используйте библиотечные процедуры, а не бросайте свои.Это вариант DRY, который я называю DRO (не повторяйте других).Кроме того, библиотечные процедуры с меньшей вероятностью будут ошибочными, чем ваша собственная реализация.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...