一、背景
在嵌入式平台里,arm64是一个非常常用的平台,arm64虽然在单核性能上要弱于x86,但是在指令集方面功能性上要更强,更有操作空间,具体来说,对于arm64v8架构的cpu平台,有SIMD的指令集的支持,使用SIMD可以做一些局部代码逻辑上的极致优化,但是,并不是所有的情形都可以用SIMD指令来提升性能,就比如memcpy这个场景,直接用纯SIMD指令比如下图中的纯SIMD指令的使用相比C库原生的memcpy并不能提升性能,甚至还略差。
本文讲的对memcpy的优化,是借助SIMD的思想,而不是直接用SIMD的命令,思路就是在arm64平台上嵌入汇编把memcpy时使用的寄存器又C库里的64位扩大到armv8支持的128位的寄存器
二、实现代码
/*
- 综合测试出来,用这个函数的性能最好,相比原memcpy有30-40%的提升
- 如果这块数据是一块新数据的话,则有更大的如到40%的提升
*/
attribute((optimize("O3")))void memcpy_neon1(void* dst, const void* src, size_t size) {
size_t i;
size_t simd_size = size / 128; // 每次复制128个字节(16个字节 * 8个向量)
size_t remainder = size % 128; // 剩余字节数
uint8_t* dst_ptr = (uint8_t*)dst;
const uint8_t* src_ptr = (const uint8_t*)src;
/*
* 这里做prefetch,其收益还是有的,可以大致的测出相比不做prefetch有5~10%的收益
* 另外c库的实现也是带着这个prfm的
*/
asm volatile("prfm pldl1keep, [%[address]]"
:
: [address] "r" (src_ptr)
: );
for (i = 0; i < simd_size; i++) {
uint8x16_t q0, q1, q2, q3, q4, q5, q6, q7;
// 从源地址加载128个字节
asm volatile("ldp %q[q0], %q[q1], [%[src]], #32\n"
"ldp %q[q2], %q[q3], [%[src]], #32\n"
"ldp %q[q4], %q[q5], [%[src]], #32\n"
"ldp %q[q6], %q[q7], [%[src]], #32\n"
: [q0] "=w"(q0), [q1] "=w"(q1), [q2] "=w"(q2), [q3] "=w"(q3),
[q4] "=w"(q4), [q5] "=w"(q5), [q6] "=w"(q6), [q7] "=w"(q7),
[src] "+r"(src_ptr));
// 将128个字节存储到目标地址
asm volatile("stp %q[q0], %q[q1], [%[dst]], #32\n"
"stp %q[q2], %q[q3], [%[dst]], #32\n"
"stp %q[q4], %q[q5], [%[dst]], #32\n"
"stp %q[q6], %q[q7], [%[dst]], #32\n"
: [dst] "+r"(dst_ptr)
: [q0] "w"(q0), [q1] "w"(q1), [q2] "w"(q2), [q3] "w"(q3),
[q4] "w"(q4), [q5] "w"(q5), [q6] "w"(q6), [q7] "w"(q7));
}
// 处理剩余的字节
memcpy(dst_ptr, src_ptr, remainder);
}
上文中的函数名字是memcpy_neon1,其实还有一个memcpy_neon0,在第三章里的测试结果里会体现他们之间的实测效果差异。memcpy_neon1和memcpy_neon0的实现基本是一摸一样的,仅仅在于memcpy_neon1使用prfm预取,也只是预取了一次,预取了多少其实和arch相关,与cache的hw prefetch配置和cache line的size都相关,这块细节会在处理器架构的后续章节里展开描述。从测试结果上来看,带上prfm性能相对更高,但是要注意,prfm不能每个循环都做,只做一次也就是首地址去做预取才能有收益,做得多实测下来还是负收益。另外,补充一点,你不用担心预取导致的数据不一致问题,因为带OS的系统上cache都有如MESI的缓冲一致性的协议,数据的变更会因为协议而及时同步到SMP里的其他核上
三、实测对比的注意事项及结果对比
3.1 实测对比需要注意的事项
1)对比测试时,需要尽量都使用新的内存,避免数据块已经拷贝一次导致的数据进入cache后读取速度变快而造成结果上的误判。或者说,可以进行时序上的颠倒,比如先测C库的memcpy,再测优化后的memcpy,测出结果以后,再返过来顺序,先测优化后的memcpy,再测C库的memcpy,来对比看,排除掉cache的影响
2)memcpy的src和dst的首地址在8字节对齐时优化后的memcpy提升的性能更加明显,如果首地址不是8字节对齐的,4字节对齐或者无对齐,提升的效果递减。这是源自于ldp和stp的读取128bit的内容时,8字节对齐的数据会让其汇编的读取执行更加的高效
3.2 实测结果对比
测试结果:
测的256到32768字节这个数据量的memcpy,使用我写的memcpy,相比C库的memcpy有30-40%的提升
下图的cost单位是ns
图中neon0和neon1是两个差不多的实现,都用了SIMD,neon0没有使用prfm,neon1使用了prfm。可以看到neon1的性能相对更高,所以最后使用的是memcpy_neon1这个实现,见第二章的实现代码
标签:src,字节,dst,SIMD,优化,memcpy,size From: https://blog.csdn.net/weixin_42766184/article/details/143327718