C语言骚操作:Duff‘s Device

目录

引言

Duff's Device

代码解析

循环展开

switch-case特性


引言

前段时间在网上看了一段代码,简直惊掉了下巴,代码贴在了下面,供大家欣赏

#include <stdio.h>

int test(int count)
{
	int i =0;
	int n=(count+7)/8;	/*假设count>0*/
	switch(count%8)
	{
		case 0: do{ i++;
		case 7:		i++;
		case 6:		i++;
		case 5:		i++;
		case 4:		i++;
		case 3:		i++;
		case 2:		i++;
		case 1:		i++;
				}while(--n>0);
	}
	return i;
}

int main(int argc,char *argv[])
{
	printf("%d\n",test(20));
	return 0;
}

如果你也是第一次看到的话,不妨试一下,看你能得出正确答案吗?

其实,上述代码源自大师之手,我只是做了少许修改。先来聊一下这段历史渊源吧。

Duff's Device

        1983年11月,一位叫Tom Duff的大牛在编写串口通信程序时,发现使用一般的写法时,性能总是不能让人满意。后来,这位老兄凭借深厚的编程功底和精湛的C语言技艺,利用C语言中switch语句的一个鲜为人知的特性,发明如了下图所示的经典代码:

void duffs_device_memcpy(char* dest, const char* src, size_t size) {
    size_t n = (size + 7) / 8; // 计算需要多少次循环展开
    switch (size % 8) {        // 处理剩余字节
        case 0: do { *dest++ = *src++;
        case 7:      *dest++ = *src++;
        case 6:      *dest++ = *src++;
        case 5:      *dest++ = *src++;
        case 4:      *dest++ = *src++;
        case 3:      *dest++ = *src++;
        case 2:      *dest++ = *src++;
        case 1:      *dest++ = *src++;
                } while (--n > 0);
    }
}

结果,引来无数吃瓜群众膜拜。在此之前,还没有人发现并利用过C语言的这个特性,于是他便以自己的名字命名这段代码,叫做Duff's Device,一般译为"达夫设备"。

一起看一下大牛的风采,1983年,在卢卡斯影业上班的程序员Tom Duff

代码解析

        当时,Duff的需求,是把一段起始地址为from,长度为count的数据,写入到一个内存映射的I/O(Memory Mapped I/O)寄存器to中。

        我相信大家都能实现这个需求,直接用for或者while循环就可以解决了

void send(uint8_t *to, uint8_t *from, uint16_t count)
{
    do {                          /* count > 0 assumed */
        *to++ = *from++;
    } while (--count > 0);
}

代码清晰简洁,很直观,简直完美,对吧?

Duff却对此很不满意,因为他觉得这种写法虽然简单,但太过低效,无法接受。

如此简单的代码,为何说它性能低下呢?主要有两个问题:

  •  "无用"指令太多

  •  无法充分发挥CPU的ILP(Instruction-Level Parallelism)技术

我们来分析一下。

所谓无用指令,是指不直接对所期望的结果产生影响的指令。

对于这段代码,我们期望的结果就是把数据都拷贝到I/O寄存器to中。那么,对于这个期望的结果来说,真正有用的代码,其实只有中间那一行赋值操作:

 *to++ = *from++;

而每次迭代过程中的while (--count > 0)产生的指令,以及每次迭代结束后的跳转指令,对结果来说都是无用指令。

上面最简单的实现中,每次循环迭代只拷贝一个字节数据。这就意味着,有多少个字节的数据,就需要执行多少次跳转和条件判断,以及--count的操作。

何为CPU的ILP(Instruction-Level Parallelism)技术

        是指处理器在单个指令流中同时执行多条指令的能力。它是现代 CPU 提高性能的关键技术之一,通过并行执行指令来充分利用硬件资源。

  • 流水线(Pipelining):将指令执行分为多个阶段,每个阶段由不同的硬件单元处理。
  • 超标量(Superscalar):在一个时钟周期内发射多条指令到多个执行单元。
  • 乱序执行(Out-of-Order Execution):根据指令的依赖关系动态调整执行顺序

一个设计合理的程序,往往能够充分利用CPU的这些ILP机制,以使性能达到最优

但是上述代码无用指令太多,且每个迭代只执行中间那一行赋值操作,无法充分发挥ILP的技术优势。

        知道上面那个简单实现性能差的原因了,那么如何去优化它呢?

循环展开

        所谓循环展开,是通过增加每次迭代内数据操作的次数,来减小迭代次数,甚至彻底消除循环迭代的一种优化手段。

循环展开,有以下优点:

  • 有效减少循环控制指令。前面说过,这些指令,是对结果不产生影响的无用指令。减少这些指令,就可以减少这些指令本身执行所需的开销,从而提升整体性能。

  • 通过合理的展开,可以更加有效地利用指令级并行ILP(Instruction-Level Parallelism 指令级并行)技术

循环展开是一个很常用的性能优化手段,所有现代编译器,通过合适的选项,都支持循环展开优化。

知道循环展开的好处,可以对上面的代码实现进行第一次优化尝试了。

我们先尝试把每次循环内拷贝字节的个数,由1个提高到到8个,这样就可以把迭代次数降低8倍。

我们先假设,send()函数的参数count总是8的倍数,那么上面的代码就可以修改为:

void send(uint8_t *to, uint8_t *from, uint16_t count)
{
    uint16_t n=count/8;  /* 假设count > 0 且是8的倍数*/
    do {                         
        *to++ = *from++;
        *to++ = *from++;
        *to++ = *from++;
        *to++ = *from++;
        *to++ = *from++;
        *to++ = *from++;
        *to++ = *from++;
        *to++ = *from++;
    } while (--n > 0);
}

上面的代码很好理解,就是把原来迭代里的操作复制了8次,然后把迭代次数降低了8倍。

但是,我们前面做了一个假设,就是count是8的倍数。那如果不是8的整数倍呢,那我们如何优化呢?

void send(uint8_t *to, uint8_t *from, uint16_t count)
{
    uint16_t n=count/8;  /* 假设count > 0 */
    do {                         
        *to++ = *from++;
        *to++ = *from++;
        *to++ = *from++;
        *to++ = *from++;
        *to++ = *from++;
        *to++ = *from++;
        *to++ = *from++;
        *to++ = *from++;
    } while (--n > 0);
    n=count%8;
    while(n-->0)
    {
         *to++ = *from++;
    }
}

优化到这里,相比原始的实现来说,性能已经大大得到了提升。但是,Duff仍然不满意,他看着第二个while循环非常不爽,尽管对整体性能已经没有太大影响了。

也许这就是大牛异于常人之处,大牛总是追求极致,总是可以在看似不可能的时候,再往前走一步。

switch-case特性

Duff注意到C语言中switch-case语句的一些特性:

  •  case语句后面的break语句不是必须的。

  •  在switch语句内,case标号可以出现在任意的子语句之前,甚至运行出现在if、for、while等语句内。

Duff便利用switch-case的特性,用来处理第一个while循环之后仍然剩余的count % 8个字节的数据。于是便有了这样的代码:

void send(uint8_t *to, uint8_t *from,uint16_t count)
{
	uint16_t n = (count+7)/8;	/*假设count>0*/
	
	switch(count%8)
	{
		case 0: do{ *to++=*from++;
		case 7:		*to++=*from++;
		case 6:		*to++=*from++;
		case 5:		*to++=*from++;
		case 4:		*to++=*from++;
		case 3:		*to++=*from++;
		case 2:		*to++=*from++;
		case 1:		*to++=*from++;
				}while(--n>0);
	}
}

我们假设count = 20,那么:

n = (count + 7) / 8 = 27 / 8 = 3
count % 8 = 4
  1. switch语句会落入case 4的标签内,然后依次执行了case 4、3、2、1四条语句。自此之后,其实就跟switch-case语句再也没有关系了。

  2. while语句判断--n > 0,条件成立,于是跳转到case 0进入循环体执行,于是依次执行case 0、7、6、5、4、3、2、1一共8条语句。此时n = 2.

  3. 再次进入while语句处判断--n >0,条件成立,再次跳转到case 0处进入循环体执行。此时n = 1

  4. 此时,while语句处判断--n >0,条件失败,退出循环,函数结束。

好了,到这里,大家应该理解Duff's Device了吧。理解了Duff's Device之后,文章开头的那个题目就很好理解了。

### C语言常用案例与示例代码 C语言作为一种功能强大且灵活的编程语言,广泛应用于系统开发、嵌入式开发以及算法实现等领域。以下是一些经典的C语言案例和示例代码: #### 1. 字符串操作 字符串是C语言中常见的数据类型之一,通过标准库函数可以轻松完成字符串长度计算、比较等操作。 ```c #include <stdio.h> #include <string.h> int main() { char str[] = "Hello, World!"; printf("字符串长度: %lu\n", strlen(str)); // 计算字符串长度[^1] return 0; } ``` #### 2. Duff's Device 示例 Duff's Device 是一种优化循环性能的经典技巧,利用了 `switch` 语句的特性,常用于批量数据处理。 ```c #include <stdio.h> void send(int *to, int *from, int count) { int n = (count + 7) / 8; // 每次处理8个元素 switch (count % 8) { case 0: do { *to++ = *from++; case 7: *to++ = *from++; case 6: *to++ = *from++; case 5: *to++ = *from++; case 4: *to++ = *from++; case 3: *to++ = *from++; case 2: *to++ = *from++; case 1: *to++ = *from++; } while (--n > 0); } } int main() { int src[8] = {1, 2, 3, 4, 5, 6, 7, 8}; int dest[8]; send(dest, src, 8); for (int i = 0; i < 8; i++) { printf("%d ", dest[i]); } return 0; } ``` 此代码展示了如何通过 `switch` 实现高性能的数据复制[^2]。 #### 3. 猜数字游戏 这是一个简单的交互式程序,用户需要猜测一个随机生成的数字,直到猜对为止。 ```c #include <stdio.h> #include <stdlib.h> #include <time.h> int main() { int num, guess, count = 0; srand(time(NULL)); num = rand() % 100 + 1; printf("猜数字游戏开始!\n"); do { printf("请输入一个1到100之间的整数:"); scanf("%d", &guess); count++; if (guess > num) { printf("太大了!\n"); } else if (guess < num) { printf("太小了!\n"); } } while (guess != num); printf("恭喜您猜对了,共猜了%d次!\n", count); return 0; } ``` 此代码实现了基本的用户输入和条件判断逻辑[^3]。 #### 4. 错误代码定义与返回值 在嵌入式开发中,错误代码的定义和使用是非常重要的,确保程序的健壮性和可维护性。 ```c typedef enum { SYS_PG_OK = 0, SYS_PG_FAIL = -1, SYS_MEM_ERR = -2, SYS_TIMEOUT = -3 } ERR_CODE_t; ERR_CODE_t ipu_power_up_then_init() { ERR_CODE_t err_code = SYS_PG_OK; // 初始化错误代码为正常状态 // ... 函数实现 ... return err_code; } ``` 上述代码展示了如何定义枚举类型以表示错误代码,并将其作为函数返回值[^4]。 --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值