我有一个函数在执行memcpy操作,但是它占用了大量的CPU周期。有没有比使用memcpy更快的替代方法或者其他方式来移动内存块?
我有一个函数在执行memcpy操作,但是它占用了大量的CPU周期。有没有比使用memcpy更快的替代方法或者其他方式来移动内存块?
memcpy
可以说是在内存中复制字节的最快方法。如果你需要更快的方式 - 尝试找到一种方法来实现不复制数据,例如仅交换指针,而非数据本身。
这是针对带有AVX2指令集的x86_64的答案。尽管类似的方法可能适用于带有SIMD的ARM / AArch64。
在Ryzen 1800X上,如果单个内存通道完全填充(2个插槽,每个插槽16 GB DDR4),则以下代码在MSVC++2017编译器上比 memcpy()
快1.56倍。如果您使用2个DDR4模块填充了两个内存通道,即您使用了所有4个DDR4插槽,则可以获得进一步的2倍快速内存复制。对于三通道-(四通道)内存系统,如果将代码扩展为类似的AVX512代码,则可以获得进一步的1.5(2.0)倍快速内存复制。对于仅支持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();
}
这段代码的一个重要特点是在复制时跳过CPU缓存:如果涉及CPU缓存(即使用AVX指令且没有_stream_
),在我的系统上,复制速度会下降数倍。
我的DDR4内存是2.6GHz CL13。因此,当从一个数组复制8GB数据到另一个数组时,我得到了以下速度:
memcpy(): 17,208,004,271 bytes/sec.
Stream copy: 26,842,874,528 bytes/sec.
请注意,在这些测量中,输入和输出缓冲区的总大小是按经过的秒数分配的。因为对于数组的每个字节,有两个内存访问:一个是从输入数组读取字节,另一个是将字节写入输出数组。换句话说,当从一个数组复制8GB到另一个数组时,您执行了16GB的内存访问操作。
适度的多线程可以进一步提高性能约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();
}
}
更新于2023-01-18:
我不再拥有那个系统,但是 2666MHz DDR4
标记为 PC4-21300U
,意味着每个 RAM 插槽传输速率为 22334668800
字节/秒。由于我有 2 个 RAM 插槽,因此最大带宽为 44669337600
字节/秒。通过使用 SIMD 和多线程的方法,当使用 5 个线程时,实现了理论带宽的 87.72%
。
_mm256_stream_load_si256
只有在复制 WC 存储区域(例如从视频 RAM)时才会执行特殊操作。否则,它只是一个更慢的(1 个额外的 uop)对你分配的正常内存的 vmovdqa
(这将是 WB = 写回高速缓存、强序的,并且 movntdqa
loads,不像 NT 存储,不会覆盖强序)。你不能绕过缓存读取正常内存,只能有时通过 NT 预取最小化污染。(但这很难调整,取决于机器,而不仅仅是代码。) - Peter Cordesrep movsb
)会是一个优势。对于小到中等规模的复制,如果您很快就要再次读取内存,则绕过缓存是一个很大的缺点。 - Peter Cordesrep movsb
。如果您的C实现的memcpy尚未这样做,或者您知道此复制应该是非暂态的,则手动执行它可能是有意义的。 - Peter Cordesmemcpy()
。memmove
必须检查和处理重叠,但不要求memcpy
这样做。更大的问题是,为了在复制大块时效率高,memcpy
的实现需要在开始工作之前选择复制方法。如果代码需要能够复制任意数量的字节,但那个数字90%的时间是1,9%的时间是2,0.9%的时间是3等等,并且在之后不需要count
、dest
和src
的值,则内联的if (count) do *dest+=*src; while(--count > 0);
可能比“更聪明”的例程更好。 - supercatmemcpy
不是最快的方法的另一个原因是 DMA 控制器有时可以比 CPU 更少地复制一块内存,但最有效的复制方式可能是启动 DMA,然后在 DMA 运行时进行其他处理。在具有单独的前端代码和数据总线的系统上,可以配置 DMA,使其在 CPU 不需要数据总线用于其他任何事情时在每个周期复制数据。这可能比使用 CPU 进行复制使用更好的性能... - supercatstart_memcpy()
和await_memcpy_complete()
函数,但任何代码通常都必须根据特定的应用要求进行定制,标准库中没有类似的内容。 - supercat static inline void newmemcpy(void *__restrict__ dstp,
void *__restrict__ srcp, uint len)
{
ulong *dst = (ulong *) dstp;
ulong *src = (ulong *) srcp;
uint i, tail;
for(i = 0; i < (len / sizeof(ulong)); i++)
*dst++ = *src++;
/*
Remove below if your application does not need it.
If console application, you can uncomment the printf to test
whether tail processing is being used.
*/
tail = len & (sizeof(ulong) - 1);
if(tail) {
//printf("tailused\n");
uchar *dstb = (uchar *) dstp;
uchar *srcb = (uchar *) srcp;
for(i = len - tail; i < len; i++)
dstb[i] = srcb[i];
}
}
std::memcpy
的调用。 - Violet Giraffememcpy
调用标准库中的memcpy
函数 - 你想要的是重复调用最佳的ASM指令来复制最大数量的数据 - 比如rep movsq
。mov
替换为调用memcpy
来优化对memcpy
的调用,只要它知道应该复制多少数据。如果你写一个具有明确定义(constexpr
)值的memcpy
,你就可以看到这一点。如果编译器不知道这个值,它将不得不退回到memcpy
的字节级实现,问题在于memcpy
必须遵守一个字节的粒度。它仍然会每次移动128位,但在每个128b之后,它必须检查是否有足够的数据作为128b或者它必须退回到64位、32位和8位(我认为16位可能不是最优的,但我不确定)。memcpy
你的数据大小是由编译器可以优化的常量表达式确定的。这样就不会执行任何对memcpy
的调用。你不想传递一个只在运行时才知道的变量给memcpy
。这将转化为一个函数调用和大量测试,以检查最佳的复制指令。有时,一个简单的for循环比memcpy
更好(消除一个函数调用)。而且,你真的真的不想传递一个奇数字节数来复制给memcpy
。如果您的平台支持,可以考虑使用mmap()系统调用将数据留在文件中...通常操作系统可以更好地管理它。正如大家一直在说的那样,尽可能避免复制;在这种情况下指针是您的朋友。