教你轻松优化 STM32CubeIDE 的内存拷贝函数的性能翻四倍
为啥要优化
STM32CubeIDE 针对 STM32F1 等 ROM 较低的 MCU 使用的 GCC 工具链所使用的 libc 库(按群友说,是 newlibc-nano)提供的这三个函数的功能实现非常拉跨:
memset()
memcpy()
memmove()
它们的问题是:
- 它跑循环进行逐个字节的处理。如果内存地址是对齐的,它的写入的部分应该按字长来处理,也就是按
uint32_t 为一个单位来处理。所以实际的处理速度是本来应该具有的处理速度的四分之一,甚至更低。
- 它们是使用汇编语言实现的,在 Release 编译的时候,无法参与 GCC 的优化,即使你开了
-flto -O3 选项,它们也不会参与优化。
- 当你开启了
-O3 ,就会导致 -ftree-loop-distribute-patterns 这一优化选项被开启。这个优化选项的作用是使编译器检查你的代码的行为,看你的代码是不是在跑循环数组拷贝、循环数据写入等,然后将你的代码实现替换为对 memset() 、memcpy() 的调用,即使你写了一个拷贝 uint32_t[] 数组的 for 循环(或者 while 循环、do 循环),也会因为这个优化选项的开启导致编译器将你的代码替换为对 memcpy() 的调用。
- 在 x86 或者高级的 ARM SoC 芯片上,因为处理器有缓存,并且 GCC 工具链使用的 libc 提供了专门针对这些处理器进行高效的内存拷贝、内存清零的函数(有的还使用了 SIMD 指令集),这个优化选项可以起到两个作用:
- 帮助你减少 CPU 的缓存交换次数,也就是将大循环分割为多个小循环来减少对缓存的使用;
- 使用 libc 库提供的高性能的
memcpy() 和 memset() 来帮你完成数据的拷贝或者清零。
- 但是这个优化选项在我们的 STM32 嵌入式开发过程中会导致负优化。
有的网友不服,说,都什么年代了还需要你用 C 语言来优化 memcpy 。既然你不服,那请看证据:
- List 文件,这个你要是看不懂的话,我还有证据,继续往下看。
- 查看执行逻辑,这个你要是也看不懂,我给你反编译成 C,你应该能看懂的吧?
- 反编译成 C,看见没,逐字节拷贝的,这就是 ST 给你提供的
memcpy 。
解决方案
- 抄我的代码(后面有提供)
- 开优化编译(编译器选项开启
-O3 优化,以及开启 -flto 优化)
- 完成!
memset() 、memcpy() 、memmove() 性能提升四倍,并且能得到编译器的特化优化和内联优化!
思路
- 重新实现这三个函数:
memset() 、memcpy() 、memmove() ,检查输入的指针是否按字长对齐,如果没有对齐,先把头部没对齐的部分按字节处理;然后把中间的部分按照对齐的方式按字长处理;最后把末尾剩余的部分(不足一个字长的部分)的数据再按字节处理即可。
- 但是 libc 已经提供了这三个函数了呀?没关系,STM32 的默认工具链使用的 libc 里面提供的标准库函数 都是弱符号 导出。你直接实现这三个函数就好了,你的函数会覆盖标准库里的函数。你的函数会被调用。
- 需要注意 全局关闭
-ftree-loop-distribute-patterns 会影响到全局的所有循环相关的代码的优化,带来副作用,有可能造成其它地方的性能降低,然而我们有办法:使用 #pragma GCC optimize ("no-tree-loop-distribute-patterns") 可以在当前源码文件局部关闭这个优化。其实,#pragma GCC optimize 的设置是可以 push 和 pop 的。可以先 push ,再关闭这个优化,然后实现我们的那三个函数,最后再 pop 恢复这个优化就行了。
- 这样一来,我们在别处写的循环拷贝、循环赋值代码可以被编译器替换为我们自己的
memset() 、memcpy() 实现,甚至能帮我们生成特化后的专用代码,把循环长度和要读写的内存地址写死在代码里,连拷贝数据的循环都可以展开。那这样可就爽了。
代码实现
第一步:新建一个 .c 文件,我这边叫它 my_memory.c ,确保这个源码文件参与编译。
第二步:复制以下代码到这个源码文件里。然后就大功告成了。
/*
* my_memory.c
*
* Created on: Jun 1, 2025
* Author: 0xaa55
*/
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
// https://gcc.gnu.org/onlinedocs/gcc/Function-Specific-Option-Pragmas.html
// https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html#index-ftree-loop-distribute-patterns
// https://stackoverflow.com/questions/46996893/gcc-replaces-loops-with-memcpy-and-memset
#pragma GCC optimize ("no-tree-loop-distribute-patterns")
void *memset(void * dst, int val, size_t len)
{
uint32_t *ptr_dst = dst;
size_t head = (size_t)ptr_dst & 0x3;
if (head)
{
uint8_t *ptr_dst_ = (uint8_t *)ptr_dst;
while(head && len)
{
*ptr_dst_ ++ = (uint8_t) val;
head --;
len --;
}
ptr_dst = (uint32_t *)ptr_dst_;
}
if (len >= 4)
{
union {
uint8_t u8[4];
uint32_t u32;
} v4;
v4.u8[0] = (uint8_t) val;
v4.u8[1] = (uint8_t) val;
v4.u8[2] = (uint8_t) val;
v4.u8[3] = (uint8_t) val;
while (len >= 4)
{
*ptr_dst ++ = v4.u32;
len -= 4;
}
}
uint8_t *ptr_dst_ = (uint8_t *)ptr_dst;
while (len)
{
*ptr_dst_ ++ = (uint8_t) val;
len --;
}
return dst;
}
void *memcpy(void *dst, const void *src, size_t len)
{
uint32_t *ptr_dst = dst;
const uint32_t *ptr_src = src;
if (dst == src) return dst;
size_t head = (size_t)dst & 0x3;
if (head)
{
uint8_t *ptr_dst_ = (uint8_t *)ptr_dst;
uint8_t *ptr_src_ = (uint8_t *)ptr_src;
while(head && len)
{
*ptr_dst_++ = *ptr_src_++;
head --;
len --;
}
ptr_dst = (uint32_t *)ptr_dst_;
ptr_src = (uint32_t *)ptr_src_;
}
while (len >= 4)
{
*ptr_dst++ = *ptr_src++;
len -= 4;
}
if (len)
{
uint8_t *ptr_dst_ = (uint8_t *)ptr_dst;
uint8_t *ptr_src_ = (uint8_t *)ptr_src;
while (len)
{
*ptr_dst_++ = *ptr_src_++;
len --;
}
}
return dst;
}
void *memmove(void * dst, const void * src, size_t len)
{
if (dst == src) return dst;
if (dst < src)
{
return memcpy(dst, src, len);
}
else
{
uint32_t *ptr_dst_end = (uint32_t *)((uint8_t*)dst + len);
uint32_t *ptr_src_end = (uint32_t *)((uint8_t*)src + len);
size_t tail = (size_t)ptr_dst_end & 0x3;
if (tail)
{
uint8_t *ptr_dst_end_ = (uint8_t *)ptr_dst_end;
uint8_t *ptr_src_end_ = (uint8_t *)ptr_src_end;
while (tail && len)
{
*--ptr_dst_end_ = *--ptr_src_end_;
tail --;
len --;
}
ptr_dst_end = (uint32_t *)ptr_dst_end_;
ptr_src_end = (uint32_t *)ptr_src_end_;
}
while (len >= 4)
{
*--ptr_dst_end = *--ptr_src_end;
len -= 4;
}
if (len)
{
uint8_t *ptr_dst_end_ = (uint8_t *)ptr_dst_end;
uint8_t *ptr_src_end_ = (uint8_t *)ptr_src_end;
while (len)
{
*--ptr_dst_end_ = *--ptr_src_end_;
len --;
}
}
}
return dst;
}
就这么多代码。保存后编译即可。Debug 不开优化的模式下,这三个函数就正常地比标准库的函数实现快四倍;Release 开了优化的模式下,那就不止快四倍了。
你只要包含了标准库里的声明了这三个函数的相关头文件(比如 stdlib.h ),你直接调用它即可调用到这个 .c 源码文件里面的函数实现。开了 -O3 优化后,编译器会针对你对这三个函数的调用,生成特化的函数来减少啰嗦;而开启了 -flto 优化后,编译器就会视情况内联这三个函数到你调用的位置。爽歪歪。
来看优化效果:
memset() 针对特定的固定位置的结构体产生的特化优化:
循环展开,看见没?
- 来看
memmove() ,请注意,我在 memmove() 顺向拷贝数据的地方调用了 memcpy() ,但是编译器把我的 memcpy() 内联进去了。
- 再来看
memcpy() ,我的代码实现是先处理对齐问题;一旦对齐了,就进行 4 字节为单位的拷贝,实际上,编译器给我生成的是按 8 字节为单位的拷贝。
我要说我的代码造成了四倍优化,都是保守的说法了,实际上是八倍的优化。
其它的函数其实也需要优化
memcmp()
strlen()
strcmp()
strcpy()
strncpy()
strcat()
- ...
不过因为这些函数一般出场率不高,我就懒得挨个都弄一下了。
|