[嵌入式C编程与固件开发] TinyMLC代码中常见优化:2、位操作优化

[复制链接]
 楼主| xu@xupt 发表于 2025-8-7 22:02 | 显示全部楼层 |阅读模式

1、问题描述
一个模型在PC上跑得好好的,一旦经过量化、部署到MCU上,最常见的“猝死”原因之一就是——内存不足。今天,分享一个实战案例,聊聊是如何通过C语言中最基础、最原始,也最强大的武器——位操作,成功将模型的关键压缩内存占用。训练的一个关键词唤醒模型本身不大,权重加起来也就几十KB,可以安稳地待在Flash里。问题就出在推理时产生的中间数据上。经过分析,发现模型中有一个激活函数的输出层,其张量尺寸是整个模型中最大的,达到了160*128,即20,480个元素。激活函数是一个“二值化”函数,作用是将前一层的计算结果,大于0的输出为1,小于等于0的输出为0。
2、问题描述
最开始,为了快速实现功能,写的代码非常“朴素”,将20,480个1或0的结果,老老实实地存放在一个uint8_t类型的数组里。

  1. #include <stdint.h>

  2. #include <stddef.h> // For size_t

  3. #define ACTIVATION_MAP_SIZE (160 * 128) // 20480

  4. // 函数功能:将浮点型输入数据进行二值化

  5. // input_data: 前一层计算出的浮点数结果

  6. // output_map: 用于存储二值化结果(0或1)的数组

  7. void binarize_activations_naive(const float* input_data, uint8_t* output_map) {

  8. for (size_ti = 0; i < ACTIVATION_MAP_SIZE; ++i) {

  9. // 直观的实现:大于0则为1,否则为0

  10. // 每个结果占用一个完整的字节(uint8_t)

  11. if (input_data[i] > 0.0f) {

  12. output_map[i] = 1;

  13. } else {

  14. output_map[i] = 0;

  15. }

  16. }

  17. }


这段代码逻辑清晰,一目了然。但来算一笔账:需要存储20,480个1或0,从信息论的角度看只是20,480个比特(bit)的信息量。但是,为了存储这20,480个结果,定义了一个uint8_t数组。每个uint8_t占用1个字节(Byte),也就是8个比特。所以,output_map数组的大小是20480*1 Byte=20,480 Bytes,即20KB。为了存储仅仅2560字节(2.5KB)的有效信息,浪费了17.5KB的宝贵SRAM!每个字节中,只用了1个bit,剩下的7个bit全都是0,成为了内存空间里无用的“填充物”。
3、优化思路
更换MCU?这意味着更高的物料成本(BOM cost)和更长的硬件验证周期。作为一名合格的嵌入式工程师,怎能轻易向硬件妥协?省钱就是赚钱,优化就是生命!优化的思路其实非常直接:既然一个字节有8个比特,为什么不能用它来存8个二值化结果呢?这,就是位操作大显身手的舞台。目标是将binarize_activations_naive函数进行彻底改造,用一个uint8_t数组来紧凑地存储所有20,480个比特,将内存占用理论上降低到原来的1/8。

  1. #include <stdint.h>

  2. #include <stddef.h> // For size_t

  3. #include <string.h> // For memset

  4. #define ACTIVATION_MAP_SIZE (160 * 128) // 20480

  5. // 优化后,存储所有比特需要的字节数

  6. #define PACKED_MAP_SIZE (ACTIVATION_MAP_SIZE / 8) // 2560

  7. // 函数功能:使用位操作,将浮点输入紧凑地二值化存储

  8. // input_data: 前一层计算出的浮点数结果

  9. // packed_output_map: 用于紧凑存储二值化结果的字节数组

  10. void binarize_activations_optimized(const float* input_data, uint8_t* packed_output_map) {

  11. // 1. 初始化输出内存为0,这是非常重要的一步!

  12. // 这样只需要在值为1时进行“置位”操作即可。

  13. memset(packed_output_map, 0, PACKED_MAP_SIZE);

  14. for (size_ti = 0; i < ACTIVATION_MAP_SIZE; ++i) {

  15. // 2. 只有当输入大于0时,才需要进行操作

  16. if (input_data[i] > 0.0f) {

  17. // 3. 计算当前结果应该存储在哪个字节 (byte)

  18. size_tbyte_index = i / 8;

  19. // 4. 计算当前结果应该存储在该字节的哪个比特 (bit)

  20. // 使用取模运算 i % 8 来确定是第0到第7个比特位

  21. // 例如 i=0 -> bit 0; i=1 -> bit 1; ... i=8 -> bit 0 of next byte

  22. uint8_t bit_index = i % 8;

  23. // 5. 核心操作:将对应比特位置1

  24. // (1U << bit_index) 生成一个掩码,如 00010000

  25. // 使用“位或”赋值操作符 |= 来更新字节

  26. packed_output_map[byte_index] |= (1U << bit_index);

  27. }

  28. }

  29. }


优化结果:创建的输出数组packed_output_map大小是2560字节,即2.5KB,将这个最大激活张量的内存占用,从20KB骤降至2.5KB。



您需要登录后才可以回帖 登录 | 注册

本版积分规则

134

主题

751

帖子

3

粉丝
快速回复 在线客服 返回列表 返回顶部

134

主题

751

帖子

3

粉丝
快速回复 在线客服 返回列表 返回顶部