我有一个正在执行 memcpy 的函数,但它占用了大量的周期。有没有比使用 memcpy 移动一块内存更快的替代方法/方法?
17 回答
memcpy
可能是您在内存中复制字节的最快方法。如果您需要更快的东西 - 尝试找出一种不复制东西的方法,例如只交换指针,而不是数据本身。
这是存在 AVX2 指令集的 x86_64 的答案。虽然类似的东西可能适用于带有 SIMD 的 ARM/AArch64。
在单个内存通道完全填满的 Ryzen 1800X 上(2 个插槽,每个 16 GB DDR4),以下代码比memcpy()
MSVC++2017 编译器快 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 缓存时(即使用不_stream_
使用的 AVX 指令),复制速度在我的系统上下降了好几倍。
我的 DDR4 内存是 2.6GHz CL13 。因此,当将 8GB 数据从一个阵列复制到另一个阵列时,我得到了以下速度:
memcpy(): 17,208,004,271 bytes/sec.
Stream copy: 26,842,874,528 bytes/sec.
请注意,在这些测量中,输入和输出缓冲区的总大小除以经过的秒数。因为对于数组的每个字节,都有 2 次内存访问:一次从输入数组读取字节,另一次将字节写入输出数组。换句话说,当从一个阵列复制 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();
}
}
请向我们提供更多详情。在 i386 架构上,memcpy 很可能是最快的复制方式。但是在编译器没有优化版本的不同架构上,最好重写 memcpy 函数。我使用汇编语言在自定义 ARM 架构上完成了这项工作。如果您传输大块内存,那么DMA可能就是您正在寻找的答案。
请提供更多详细信息 - 架构、操作系统(如果相关)。
通常,编译器附带的标准库memcpy()
已经为目标平台实现了最快的方法。
实际上,memcpy 并不是最快的方法,尤其是如果您多次调用它。我也有一些我真的需要加速的代码,而 memcpy 很慢,因为它有太多不必要的检查。例如,它检查目标和源内存块是否重叠,以及是否应该从块的后面而不是前面开始复制。如果你不关心这些考虑,你当然可以做得更好。我有一些代码,但这里可能是一个更好的版本:
如果您搜索,您也可以找到其他实现。但是为了真正的速度,你需要一个汇编版本。
根本不制作副本通常会更快。您是否可以调整您的功能以不复制我不知道,但值得研究。
有时像 memcpy、memset 等函数以两种不同的方式实现:
- 曾经作为一个真正的功能
- 曾经作为一些立即内联的程序集
并非所有编译器都默认采用内联汇编版本,您的编译器可能默认使用函数变体,由于函数调用而导致一些开销。检查您的编译器以了解如何采用函数的内在变体(命令行选项、编译指示...)。
编辑:有关Microsoft C 编译器的内在函数说明,请参阅http://msdn.microsoft.com/en-us/library/tzkfha43%28VS.80%29.aspx 。
您应该检查为您的代码生成的汇编代码。你不想要的是让memcpy
调用生成memcpy
对标准库中函数的调用——你想要的是重复调用最好的 ASM 指令来复制最大量的数据——比如rep movsq
.
你怎么能做到这一点?好吧,只要编译器知道应该复制多少数据,编译器就会memcpy
通过用 simple s 替换它来优化调用。mov
如果您memcpy
使用确定的 ( constexpr
) 值编写 a,您可以看到这一点。如果编译器不知道该值,它将不得不回退到字节级的实现memcpy
——问题是memcpy
必须尊重一字节的粒度。它仍然一次移动 128 位,但是在每个 128b 之后,它必须检查它是否有足够的数据复制为 128b,或者它必须回退到 64 位,然后到 32 和 8(我认为 16 可能不是最佳的无论如何,但我不确定)。
因此,您想要的是能够memcpy
使用编译器可以优化的 const 表达式来判断数据的大小。这样就不会memcpy
执行任何调用。您不想要的是传递给memcpy
仅在运行时才知道的变量。这转化为函数调用和大量测试来检查最佳复制指令。有时,一个简单的 for 循环比memcpy
这个原因更好(消除一个函数调用)。而你真正不想要的是传递给memcpy
奇数个字节来复制。
这是可内联的 memcpy 的替代 C 版本,我发现在我使用它的应用程序中,它的性能比用于 Arm64 的 GCC 的 memcpy 高出约 50%。它独立于 64 位平台。如果使用实例不需要它以提高速度,则可以删除尾部处理。复制 uint32_t 数组,较小的数据类型未经测试但可能有效。可能能够适应其他数据类型。64 位复制(同时复制两个索引)。32 位也应该可以工作,但速度较慢。归功于 Neoscrypt 项目。
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];
}
}
如果您的平台支持它,请查看是否可以使用 mmap() 系统调用将数据保留在文件中......通常操作系统可以更好地管理它。而且,正如大家所说,尽可能避免复制;在这种情况下,指针是你的朋友。
如果 memcpy 的性能已成为您的问题,我假设您必须有大量内存要复制?
在这种情况下,我同意 nos 的建议,即想办法不复制东西..
与其在需要更改时复制一大块内存,不如尝试一些替代数据结构。
在不真正了解您的问题领域的情况下,我建议您仔细研究持久性数据结构并实现您自己的数据结构或重用现有实现。
你可能想看看这个:
http://www.danielvik.com/2010/02/fast-memcpy-in-c.html
我会尝试的另一个想法是使用 COW 技术来复制内存块,并让操作系统在页面被写入后立即处理按需复制。这里有一些使用提示mmap()
:我可以在 Linux 中进行写时复制 memcpy 吗?
如果其中一个指针(输入参数)未与 32 位对齐,则此函数可能会导致数据中止异常。
这是 Visual C++/Ryzen 1700 的一些基准测试。
基准测试从 128 MiB 环形缓冲区复制 16 KiB(非重叠)数据块 8*8192 次(总共复制了 1 GiB 数据)。
然后我将结果归一化,这里我们以毫秒为单位显示挂钟时间和 60 Hz 的吞吐量值(即此函数在 16.667 毫秒内可以处理多少数据)。
memcpy 2.761 milliseconds ( 772.555 MiB/frame)
如您所见,内置memcpy
速度很快,但速度有多快?
64-wide load/store 39.889 milliseconds ( 427.853 MiB/frame)
32-wide load/store 33.765 milliseconds ( 505.450 MiB/frame)
16-wide load/store 24.033 milliseconds ( 710.129 MiB/frame)
8-wide load/store 23.962 milliseconds ( 712.245 MiB/frame)
4-wide load/store 22.965 milliseconds ( 743.176 MiB/frame)
2-wide load/store 22.573 milliseconds ( 756.072 MiB/frame)
1-wide load/store 35.032 milliseconds ( 487.169 MiB/frame)
以上只是下面的代码,其中包含n
.
// n is the "wideness" from the benchmark
auto src = (__m128i*)get_src_chunk();
auto dst = (__m128i*)get_dst_chunk();
for (int32_t i = 0; i < (16 * 1024) / (16 * n); i += n) {
__m128i temp[n];
for (int32_t i = 0; i < n; i++) {
temp[i] = _mm_loadu_si128(dst++);
}
for (int32_t i = 0; i < n; i++) {
_mm_store_si128(src++, temp[i]);
}
}
这些是我对结果的最佳猜测。根据我对 Zen 微架构的了解,它每个周期只能获取 32 个字节。这就是为什么我们最多使用 2x 16 字节的加载/存储。
- 1x 将字节加载到
xmm0
128 位 - 2x 将字节加载到
ymm0
256 位
这就是为什么它的速度大约是原来的两倍,并且在内部确切地做了什么memcpy
(或者如果您为您的平台启用了正确的优化,它应该做什么)。
由于我们现在受到缓存带宽的限制,因此也无法使其更快。我认为这是一个非常重要的事实,因为如果您受内存限制并寻找更快的解决方案,您将需要很长时间。
CPU的命令集通常支持内存到内存,而memcpy通常会使用它。这通常是最快的方法。
您应该检查您的 CPU 到底在做什么。在 Linux 上,使用 sar -B 1 或 vmstat 1 或通过查看 /proc/memstat 来观察交换进出和虚拟内存的有效性。您可能会看到您的副本必须推出大量页面以释放空间,或将它们读入等。
这意味着您的问题不在于您用于复制的内容,而在于您的系统如何使用内存。您可能需要减少文件缓存或提前开始写出,或将页面锁定在内存中等。
当我写另一个答案时,这个问题已经 12 岁了。但随后它仍然出现在搜索中,并且答案总是在不断发展。
令人惊讶的是,还没有人提到 Agner Fog 的asmlib。
替换memcpy()以及许多其他 SIMD 优化的 C lib 替换,如memmove()、memset()、strlen()等。
将自动使用您的 CPU 支持的最佳 AVX-512 指令集。带有用于多个 x86/AMD64 平台的预构建库。