打印
[嵌入式C编程与固件开发]

[嵌入式C编程与固件开发]使用指针和字长操作快速清零数组

[复制链接]
154|0
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
yinxiangxv|  楼主 | 2025-8-6 18:46 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

假设我们需要将一个int类型的数组清零。

版本 1:朴素的循环实现 (未优化)

这是最直观的写法,初学者通常会这样写:

#include <stdint.h>
#define ARRAY_SIZE 128

uint32_t data_array[ARRAY_SIZE];

void clear_array_naive() {
    for (int i = 0; i < ARRAY_SIZE; i++) {
        data_array[i] = 0;
    }
}

这段代码逐个元素地将数组清零。在大多数编译器中,它会被优化,但我们仍有手动优化的空间,特别是在一些优化能力较弱的嵌入式编译器上。

版本 2:结合指针和

使用指针代替数组下标访问,可以减少每次循环中计算地址的开销。

#include <stdint.h>
#define ARRAY_SIZE 128

uint32_t data_array[ARRAY_SIZE];

void clear_array_pointer() {
    uint32_t *ptr = data_array;
    // 使用 sizeof 计算迭代次数,更具通用性
    int count = sizeof(data_array) / sizeof(data_array[0]);
   
    for (int i = 0; i < count; i++) {
        *ptr++ = 0;
    }
}

*ptr++ = 0; 这行代码做了两件事:1. 将ptr指向的地址写入0;2. 将ptr指针移动到下一个元素。这通常比data_array更高效。

版本 3:终极优化 - 利用CPU字长进行“块”操作

这是精髓所在。现代CPU(如ARM Cortex-M系列)通常是32位或64位的。这意味着CPU在一个时钟周期内可以处理32位(4字节)或64位(8字节)的数据。如果我们仍然以char(1字节)或int(4字节)为单位进行操作,就没有充分利用CPU的“带宽”。

这个优化思路是:将内存操作的单位从“字节”提升到CPU的“字长”

假设我们运行在一个32位CPU上,并且我们想清零一个大块内存。我们可以将指向内存的指针强制转换为指向uint32_t(32位无符号整数)的指针,然后以4字节为单位进行写入。

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

// 假设我们有一个更大的、字节为单位的缓冲区
#define BUFFER_SIZE 1024
uint8_t buffer[BUFFER_SIZE];

/**
* [url=home.php?mod=space&uid=247401]@brief[/url] 使用字长对齐的方式快速清零一块内存区域
* @param p_dest 目标内存区域的指针
* @param size   要清零的字节数
*/
void fast_memory_clear(void *p_dest, size_t size) {
    // 1. 将通用指针转换为我们可以进行算术运算的指针
    uint8_t *dest = (uint8_t *)p_dest;
    size_t count = size;

    // 2. 先按字节处理,直到地址对齐到4字节(32位)边界
    //    `((uintptr_t)dest & 3)` 这是一个位操作技巧,用于检查地址的低2位。
    //    如果地址是4的倍数,低2位必然是00。
    while (count > 0 && ((uintptr_t)dest & 3) != 0) {
        *dest++ = 0;
        count--;
    }

    // 3. 按“字”(4字节)为单位进行快速清零
    uint32_t *dest_word = (uint32_t *)dest;
    size_t word_count = count / 4;
    for (size_t i = 0; i < word_count; i++) {
        *dest_word++ = 0;
    }

    // 4. 处理剩余的不足一个“字”的字节
    dest = (uint8_t *)dest_word;
    count %= 4;
    while (count > 0) {
        *dest++ = 0;
        count--;
    }
}

// 使用示例
void run_example() {
    fast_memory_clear(buffer, BUFFER_SIZE);
}
优化效果与原理解释

这段 fast_memory_clear 函数的优化效果体现在以下几个方面:

  • 减少循环次数,提升CPU效率

    • 核心思想:对于一个1024字节的缓冲区,朴素的memset或逐字节清零需要执行1024次循环和1024次写操作。

    • 优化后:在地址对齐后,fast_memory_clear 将大部分工作交给了以uint32_t为单位的循环。它只需要执行 1024 / 4 = 256 次循环。循环次数减少了75%

    • 效果:由于循环本身(如 i++、条件判断、跳转)也需要CPU指令,大幅减少循环次数能显著降低这部分开销。更重要的是,每次写操作都利用了CPU的32位数据总线,在同样的时间内完成了4倍的工作量


  • 利用位操作技巧实现高效对齐检查

    • 代码:((uintptr_t)dest & 3) != 0

    • 解释

      • (uintptr_t)dest: 这是一个安全的类型转换,将指针地址转换为一个足够大的整数类型,以便进行数学和位运算。

      • & 3: 3 的二进制表示是 00...0011。任何数与它进行“按位与”操作,效果是只保留这个数的最低两位。

      • 原理:一个地址如果是4的倍数(即4字节对齐),那么它的二进制表示的最低两位必然是 00。例如:地址0x...00 (& 0011 -> 00), 地址0x...04 (& 0011 -> 00), 地址0x...08 (& 0011 -> 00)。而对于非对齐地址,如0x...01 (& 0011 -> 01), 0x...02 (& 0011 -> 10), 0x...03 (& 0011 -> 11),结果都不为0。


    • 效果:这是一种比使用取模运算符 % (dest % 4 != 0) 快得多的方法来检查地址对齐。因为位操作通常只需要一个CPU时钟周期,而整数除法/取模是非常慢的指令(可能需要几十个周期)。


  • 代码的健壮性 (Robustness)

    • 这个实现考虑了所有情况:

      • 起始地址非对齐。

      • 内存块大小不是4的整数倍。


    • 它通过“头部字节处理 -> 中间块处理 -> 尾部字节处理”的三段式方法,确保了无论输入是什么,都能正确、高效地完成任务。



结论

这段代码是嵌入式C语言中一个典型的优化范例。它展示了如何通过深入理解底层硬件(CPU字长、指令效率)和巧妙运用C语言特性(指针转换、位操作),编写出比标准库函数(在某些非优化编译器上)或朴素实现性能更高的代码。


使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

50

主题

335

帖子

0

粉丝