Это ответ для 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();
}
}