找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 514|回复: 2

【STM32】教你轻松优化 STM32CubeIDE 的内存拷贝函数的性能翻四倍

[复制链接]
发表于 2025-6-2 01:54:31 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有账号?立即注册→加入我们

×

教你轻松优化 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 文件,这个你要是看不懂的话,我还有证据,继续往下看。

list.png
  • IDA 找到 memcpy
ida.png
  • 定位 memcpy
memcpy2.png
  • 查看执行逻辑,这个你要是也看不懂,我给你反编译成 C,你应该能看懂的吧?
memcpy.png
  • 反编译成 C,看见没,逐字节拷贝的,这就是 ST 给你提供的 memcpy
memcpy3.png

解决方案

  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 的设置是可以 pushpop 的。可以先 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 优化后,编译器就会视情况内联这三个函数到你调用的位置。爽歪歪。

来看优化效果:

  • 针对调用场合进行的特化优化:

speci.png
  • memset() 针对特定的固定位置的结构体产生的特化优化:

speci2.png

循环展开,看见没?

  • 来看 memmove(),请注意,我在 memmove() 顺向拷贝数据的地方调用了 memcpy(),但是编译器把我的 memcpy() 内联进去了。

inline.png
  • 再来看 memcpy(),我的代码实现是先处理对齐问题;一旦对齐了,就进行 4 字节为单位的拷贝,实际上,编译器给我生成的是按 8 字节为单位的拷贝。

64bit.png

我要说我的代码造成了四倍优化,都是保守的说法了,实际上是八倍的优化。

其它的函数其实也需要优化

  • memcmp()
  • strlen()
  • strcmp()
  • strcpy()
  • strncpy()
  • strcat()
  • ...

不过因为这些函数一般出场率不高,我就懒得挨个都弄一下了。

回复

使用道具 举报

发表于 2025-6-2 11:25:16 | 显示全部楼层
以前单片机代码基于大小优化的多,然后里面的代码十几年也不怎么改
回复 赞! 靠!

使用道具 举报

发表于 2025-6-2 12:29:47 | 显示全部楼层
特定场景是会需要此优化的,顶一下,偷了
回复 赞! 靠!

使用道具 举报

本版积分规则

QQ|Archiver|小黑屋|技术宅的结界 ( 滇ICP备16008837号 )|网站地图

GMT+8, 2025-6-15 03:01 , Processed in 0.041715 second(s), 28 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表