более быстрая альтернатива memcpy? - PullRequest
37 голосов
/ 03 июня 2010

У меня есть функция, которая выполняет memcpy, но она занимает огромное количество циклов. Есть ли более быстрый альтернативный подход / подход, чем использование memcpy для перемещения фрагмента памяти?

Ответы [ 17 ]

118 голосов
/ 03 июня 2010

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

16 голосов
/ 06 июля 2017

Это ответ для x86_64 при наличии набора инструкций AVX2. Хотя что-то подобное может применяться для ARM / AArch64 с SIMD.

На Ryzen 1800X с полностью заполненным одним каналом памяти (2 слота, 16 ГБ DDR4 в каждом) следующий код в 1,56 раза быстрее, чем memcpy() на компиляторе MSVC ++ 2017. Если вы заполняете оба канала памяти двумя модулями DDR4, т. Е. У вас заняты все 4 слота DDR4, вы можете в 2 раза быстрее копировать память. Для трех- (четырех-) канальных систем памяти вы можете получить в 1,5 (2,0) раза более быстрое копирование памяти, если код будет расширен до аналогичного кода AVX512. В трехканальных / четырехканальных системах только с AVX2 со всеми занятыми слотами не ожидается, что они будут быстрее, потому что для их полной загрузки необходимо загружать / хранить более 32 байтов одновременно (48 байтов для трех- и 64-байтовых для четырехканального). систем), в то время как AVX2 может загружать / хранить не более 32 байтов одновременно. Хотя многопоточность в некоторых системах может облегчить это без AVX512 или даже AVX2.

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

Для блоков не кратного размера и не выровненных можно записать код пролога / эпилога, уменьшив ширину до 16 (SSE4.1), 8, 4, 2 и, наконец, до 1 байта за раз для головки и хвоста блока. Также в середине локальный массив из 2-3 __m256i значений может использоваться как прокси между выровненными чтениями из источника и выровненными записями в место назначения.

#include <immintrin.h>
#include <cstdint>
/* ... */
void fastMemcpy(void *pvDest, void *pvSrc, size_t nBytes) {
  assert(nBytes % 32 == 0);
  assert((intptr_t(pvDest) & 31) == 0);
  assert((intptr_t(pvSrc) & 31) == 0);
  const __m256i *pSrc = reinterpret_cast<const __m256i*>(pvSrc);
  __m256i *pDest = reinterpret_cast<__m256i*>(pvDest);
  int64_t nVects = nBytes / sizeof(*pSrc);
  for (; nVects > 0; nVects--, pSrc++, pDest++) {
    const __m256i loaded = _mm256_stream_load_si256(pSrc);
    _mm256_stream_si256(pDest, loaded);
  }
  _mm_sfence();
}

Ключевой особенностью этого кода является то, что он пропускает кэш ЦП при копировании: когда задействован кэш ЦП (т. Е. Используются инструкции AVX без _stream_), скорость копирования в моей системе несколько раз падает.

Моя память DDR4 имеет частоту 2,6 ГГц CL13. Поэтому при копировании 8 ГБ данных из одного массива в другой я получаю следующие скорости:

memcpy(): 17 208 004 271 bytes/sec.
Stream copy: 26 842 874 528 bytes/sec.

Обратите внимание, что в этих измерениях общий размер входного и выходного буферов делится на количество прошедших секунд. Поскольку для каждого байта массива есть 2 обращения к памяти: один для чтения байта из входного массива, другой для записи байта в выходной массив. Другими словами, копируя 8 ГБ из одного массива в другой, вы выполняете операции доступа к памяти на 16 ГБ.

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

Stream copy 1 threads: 27114820909.821 bytes/sec
Stream copy 2 threads: 37093291383.193 bytes/sec
Stream copy 3 threads: 39133652655.437 bytes/sec
Stream copy 4 threads: 39087442742.603 bytes/sec
Stream copy 5 threads: 39184708231.360 bytes/sec
Stream copy 6 threads: 38294071248.022 bytes/sec
Stream copy 7 threads: 38015877356.925 bytes/sec
Stream copy 8 threads: 38049387471.070 bytes/sec
Stream copy 9 threads: 38044753158.979 bytes/sec
Stream copy 10 threads: 37261031309.915 bytes/sec
Stream copy 11 threads: 35868511432.914 bytes/sec
Stream copy 12 threads: 36124795895.452 bytes/sec
Stream copy 13 threads: 36321153287.851 bytes/sec
Stream copy 14 threads: 36211294266.431 bytes/sec
Stream copy 15 threads: 35032645421.251 bytes/sec
Stream copy 16 threads: 33590712593.876 bytes/sec

Код:

void AsyncStreamCopy(__m256i *pDest, const __m256i *pSrc, int64_t nVects) {
  for (; nVects > 0; nVects--, pSrc++, pDest++) {
    const __m256i loaded = _mm256_stream_load_si256(pSrc);
    _mm256_stream_si256(pDest, loaded);
  }
}

void BenchmarkMultithreadStreamCopy(double *gpdOutput, const double *gpdInput, const int64_t cnDoubles) {
  assert((cnDoubles * sizeof(double)) % sizeof(__m256i) == 0);
  const uint32_t maxThreads = std::thread::hardware_concurrency();
  std::vector<std::thread> thrs;
  thrs.reserve(maxThreads + 1);

  const __m256i *pSrc = reinterpret_cast<const __m256i*>(gpdInput);
  __m256i *pDest = reinterpret_cast<__m256i*>(gpdOutput);
  const int64_t nVects = cnDoubles * sizeof(*gpdInput) / sizeof(*pSrc);

  for (uint32_t nThreads = 1; nThreads <= maxThreads; nThreads++) {
    auto start = std::chrono::high_resolution_clock::now();
    lldiv_t perWorker = div((long long)nVects, (long long)nThreads);
    int64_t nextStart = 0;
    for (uint32_t i = 0; i < nThreads; i++) {
      const int64_t curStart = nextStart;
      nextStart += perWorker.quot;
      if ((long long)i < perWorker.rem) {
        nextStart++;
      }
      thrs.emplace_back(AsyncStreamCopy, pDest + curStart, pSrc+curStart, nextStart-curStart);
    }
    for (uint32_t i = 0; i < nThreads; i++) {
      thrs[i].join();
    }
    _mm_sfence();
    auto elapsed = std::chrono::high_resolution_clock::now() - start;
    double nSec = 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count();
    printf("Stream copy %d threads: %.3lf bytes/sec\n", (int)nThreads, cnDoubles * 2 * sizeof(double) / nSec);

    thrs.clear();
  }
}
11 голосов
/ 03 июня 2010

Пожалуйста, предложите нам более подробную информацию. На архитектуре i386 вполне возможно, что memcpy - самый быстрый способ копирования. Но на другой архитектуре, для которой компилятор не имеет оптимизированной версии, лучше всего переписать свою функцию memcpy. Я сделал это на собственной архитектуре ARM, используя язык ассемблера. Если вы передаете БОЛЬШИЕ порции памяти, то DMA , вероятно, является ответом, который вы ищете.

Пожалуйста, предоставьте более подробную информацию - архитектура, операционная система (если применимо).

6 голосов
/ 25 января 2013

На самом деле, memcpy - НЕ самый быстрый способ, особенно если вы вызываете его много раз. У меня также был некоторый код, который мне действительно нужен для ускорения, и memcpy работает медленно, потому что в нем слишком много ненужных проверок. Например, он проверяет, перекрываются ли блоки памяти назначения и источника, и должно ли начинаться копирование с задней части блока, а не с лицевой стороны. Если вас не волнуют такие соображения, вы, безусловно, можете добиться значительно лучших результатов. У меня есть какой-то код, но вот, пожалуй, лучшая версия:

Очень быстрый memcpy для обработки изображений? .

Если вы ищете, вы можете найти и другие реализации. Но для истинной скорости вам нужна сборочная версия.

6 голосов
/ 03 июня 2010

Обычно стандартная библиотека, поставляемая с компилятором, реализует memcpy() самый быстрый из возможных способов для целевой платформы.

4 голосов
/ 08 июня 2010

Agner Fog имеет быструю реализацию memcpy http://www.agner.org/optimize/#asmlib

3 голосов
/ 03 июня 2010

Иногда такие функции, как memcpy, memset, ... реализуются двумя различными способами:

  • один раз как реальная функция
  • один раз как какая-то сборка, которая сразу вставляется

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

Редактировать: См. http://msdn.microsoft.com/en-us/library/tzkfha43%28VS.80%29.aspx для объяснения встроенных функций в компиляторе Microsoft C.

3 голосов
/ 03 июня 2010

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

2 голосов
/ 03 июня 2010

Если ваша платформа поддерживает это, посмотрите, можете ли вы использовать системный вызов mmap (), чтобы оставить ваши данные в файле ... обычно ОС справляется с этим лучше. И, как все говорили, избегайте копирования, если это возможно; указатели - ваш друг в таких случаях.

2 голосов
/ 03 июня 2010

Проверьте ваше руководство по компилятору / платформе. Для некоторых микропроцессоров и DSP-комплектов использование memcpy намного медленнее, чем встроенные функции или DMA .

...