📁位运算的奇技淫巧:builtin内建函数

什么是内建函数:

GCC 编译器自带的内建函数。这些_builtin*形式的内建函数一般是基于不同硬件平台采用专门的硬件指令实现的,因此性能较高。


介绍一些常用的内建函数:

__builtin_ctz()

返回从最低位开始(右起)的连续 0 的个数

根据数据类型不同有:

int __builtin_ctz (unsigned int x)
int __builtin_ctzl (unsigned long x)
int __builtin_ctzll (unsigned long long x)

参数 x 虽然都是无符号整型,但是值必须大于0(0 怎么也找不到 1 吧),否则会发生无法定义的后果。

example:

unsigned int x = 0x0820; // 0000 1000 0010 0000
unsigned int zero = __builtin_ctz(x); // zero = 5

__builtin_clz()

返回从最高位开始(左起)的连续 0 的个数

根据数据类型不同有:

int __builtin_clz (unsigned int x)
int __builtin_clzl (unsigned long x)
int __builtin_clzll (unsigned long long x)

参数 x 虽然都是无符号整型,但是值必须大于0(0 怎么也找不到 1 吧),否则会发生无法定义的后果。

example:

unsigned int x = 0x0820; // 0000 1000 0010 0000
unsigned int zero = __builtin_clz(x); // zero = 4

__builtin_ffs()

返回输入数二进制表示的最低非 0 位的下标,下标从 1 开始计数;如果传入 0 则返回 0。

根据数据类型不同有:

int __builtin_ffs (unsigned int x)
int __builtin_ffsl (unsigned long x)
int __builtin_ffsll (unsigned long long x)

example:

unsigned int x = 0x0820; // 0000 1000 0010 0000
unsigned int index = __builtin_ffs(x); // index = 5

除 0 外,发现传入其他的数,值就是 __builtin_ctz()+1,实际函数实现也是如此。

__builtin_popcount()

返回输入的二进制表示中 1 的个数;如果传入 0 则返回 0 。

根据数据类型不同有:

int __builtin_popcount (unsigned int x)
int __builtin_popcountl (unsigned long x)
int __builtin_popcountll (unsigned long x)

example:

unsigned int x = 0x0820; // 0000 1000 0010 0000
unsigned int count = __builtin_popcount(x); // count = 2

解决实际项目的例子

使用两字节(16 位)即可控制 16 个任意 IO 口的输出状态

例如设立一个 uint16_t ioCtrl = 0xC8A6; // 1100 1000 1010 0110,那么就是对第 1、2、5、9、11、14、15 位 IO 进行控制,再设立一个 uint16_t ioValue = 0x0C9D; // 0000 1100 1001 1101,来表示 16 位 IO 的状态。

常规的写法是使用循环来对每一个控制的 IO 写入值:

void IOCtrl_WriteIO(uint16_t ioCtrl, uint16_t ioValue)
{
    for (uint16_t i = 0; i < 16; i++)
    {
	if (ioCtrl & (1 << i))
	{
	    WriteIO(IO[i].port, IO[i].pin, ioValue & (1 << i));
	}
    }
}

这样很简单的就实现使用两字节就管理了 16 个 IO 的状态,但是我们发现即使只控制 1 个 IO,例如 0x0800,也需要去遍历整个 16 位效率很低,当结合内建函数来写:

void IOCtrl_WriteIO(uint16_t ioCtrl, uint16_t ioValue)
{
    while (ioCtrl)
    {
        // 找到 ioCtrl 最低的置位 bit
        uint16_t i = __builtin_ctz(ioCtrl);

        WriteIO(IO[i].port, IO[i].pin, (ioValue >> i) & 1);

        ioCtrl &= ioCtrl - 1; // 将最后一位1置0
    }
}

只会循环 1 的个数次数,而不用每次都遍历整个 16 位。

性能实测对比
#include <stdio.h>
#include <stdint.h>
#include <time.h>

#define N 1000000  // 测试次数

// 模拟 IO 结构
typedef struct {
    int port;
    int pin;
} IO_t;

IO_t IO[16];

// 模拟 WriteIO (不做实际 IO,只是累加,防止编译器优化掉)
volatile int dummy = 0;
void WriteIO(int port, int pin, int value)
{
    dummy += port + pin + value;
}

// 原始循环版
void IOCtrl_WriteIO_loop(uint16_t ioCtrl, uint16_t ioValue)
{
    for (uint16_t i = 0; i < 16; i++)
    {
        if (ioCtrl & (1 << i))
        {
            WriteIO(IO[i].port, IO[i].pin, ioValue & (1 << i));
        }
    }
}

// __builtin_ctz 优化版
void IOCtrl_WriteIO_ctz(uint16_t ioCtrl, uint16_t ioValue)
{
    while (ioCtrl)
    {
        uint16_t i = __builtin_ctz(ioCtrl);
        WriteIO(IO[i].port, IO[i].pin, (ioValue >> i) & 1);
        ioCtrl &= ioCtrl - 1;
    }
}

int main()
{
    clock_t start, end;
    double time_loop, time_ctz;

    // 初始化 IO
    for (int i = 0; i < 16; i++) {
        IO[i].port = i;
        IO[i].pin = i;
    }

    // 测试循环版
    start = clock();
    for (int i = 0; i < N; i++) {
        IOCtrl_WriteIO_loop(0xC8A6, 0x0C9D);
    }
    end = clock();
    time_loop = (double)(end - start) / CLOCKS_PER_SEC;

    // 测试 ctz 版
    start = clock();
    for (int i = 0; i < N; i++) {
        IOCtrl_WriteIO_ctz(0xC8A6, 0x0C9D);
    }
    end = clock();
    time_ctz = (double)(end - start) / CLOCKS_PER_SEC;

    printf("Loop version time: %f s\n", time_loop);
    printf("CTZ  version time: %f s\n", time_ctz);

    return 0;
}

1759115438143

使用内建函数速度比一般循环快 1.5 倍,短数据下提升更是明显。

扩展
ioCtrl &= ioCtrl - 1是怎么将最后一位 1 置 0 的?

假设 ioCtrl = 0xA8; // 1010 1000

0xA8 - 1 = 0xA7,转成 2 进制后是 1010 0111

0xA8:1010 1000

0xA7:1010 0111

位与操作后就变成了 1010 0000,如此就将最后一位 1 置 0 了。

ARMCC 使用

一开始说到内建函数是 GCC 编译器的函数,在嵌入式领域中,ARMCC 编译器是最常见的,在 ARMCC 编译器中怎么实现类似效果?

GCC ARMCC
__builtin_ctz(x) clz(rbit(x))
__builtin_clz(x) __clz(x)
__builtin_ffs(x) ctz(x) + 1
__builtin_popcount(x) __popcount(x)
如何判断一个数是否是 2 的幂?

在位运算里,一个数如果是 2 的幂,那么它的二进制形式一定是:

1, 10, 100, 1000, ...

如果这个数是 2 的幂,那么这个数一定只存在一个 1,对最后一位 1 置 0 后,这个数就变成了 0

bool isPowerOfTwo(uint32_t x)
{
    return x & (x - 1) == 0;
}

前面提过内联函数 __builtin_popcount()可以返回 1 的个数,使用此内联函数也可快速判断是否是 2 的幂

bool isPowerOfTwo(uint32_t x)
{
    return __builtin_popcount(x) == 1;
}
posted @ 2025-09-28 22:38  菩萨野蛮  阅读(147)  评论(0)    收藏  举报