【STM32】教你轻松优化 STM32CubeIDE 的内存拷贝函数的性能翻四倍
# 教你轻松优化 STM32CubeIDE 的内存拷贝函数的性能翻四倍## 为啥要优化
STM32CubeIDE 针对 STM32F1 等 ROM 较低的 MCU 使用的 GCC 工具链所使用的 libc 库(按群友说,是 newlibc-nano)提供的这三个函数的功能实现非常拉跨:
- `memset()`
- `memcpy()`
- `memmove()`
### 它们的问题是:
1. 它跑循环进行逐个字节的处理。如果内存地址是对齐的,它的写入的部分应该按字长来处理,也就是按 `uint32_t` 为一个单位来处理。所以实际的处理速度是本来应该具有的处理速度的四分之一,甚至更低。
2. 它们是使用汇编语言实现的,在 Release 编译的时候,无法参与 GCC 的优化,即使你开了 `-flto` `-O3` 选项,它们也不会参与优化。
3. 当你开启了 `-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 文件,这个你要是看不懂的话,我还有证据,继续往下看。
* IDA 找到 `memcpy`
* 定位 `memcpy`
* 查看执行逻辑,这个你要是也看不懂,我给你反编译成 C,你应该能看懂的吧?
* 反编译成 C,看见没,逐字节拷贝的,这就是 ST 给你提供的 `memcpy`。
## 解决方案
1. 抄我的代码(后面有提供)
2. 开优化编译(编译器选项开启 `-O3` 优化,以及开启 `-flto` 优化)
3. 完成!`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;
uint32_t u32;
} v4;
v4.u8 = (uint8_t) val;
v4.u8 = (uint8_t) val;
v4.u8 = (uint8_t) val;
v4.u8 = (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()`
* ...
不过因为这些函数一般出场率不高,我就懒得挨个都弄一下了。 以前单片机代码基于大小优化的多,然后里面的代码十几年也不怎么改 特定场景是会需要此优化的,顶一下,偷了:D:D
页:
[1]