20行经典C代码,很多人看不明白,你来试一下?

c语言基础 因为本人是新手,所以本频道服务于小白,勿喷~~~~ 阅读详情

引言

昨天发了一段有趣的代码,引来很多童鞋围观。很多童鞋表示不太明白,于是就有了本文,详细解释下这段代码的来龙去脉。

代码如下图所示:

图片

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

其实,这个题目还是源自大师之手,我只是做了少许修改。先来聊一下这段历史渊源吧。

注:为了尽量解释清楚,篇幅有点长,请耐心读完,相信你会有收获的!

历史渊源

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

图片

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

先看一下大牛的风采吧:

图片

下面讲解一下这段代码。

Duff's Device - 达夫设备

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

最简单的实现

需求很简单,对吧?很容易想到直接用for或者while循环就可以解决了,如下图所示:

图片

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

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

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

  • • "无用"指令太多

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

我们来分析一下。

无用指令太多

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

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

*to = *from++;

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

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

我们看一下汇编代码:

图片

有些童鞋对汇编不太熟悉,我简单讲解一下:

  • • x64上优先使用寄存器传递,对于send()函数,第一个参数to存放在寄存器rdi中,第二个参数from存放在rsi中,第三个参数count存放在寄存器edx中。

  • • 第2~7行,把三个参数分别压入栈中;

  • • 第9~14行,对应C语言的*to = *from++;

  • • 第15~19行,对应C语言的while (--count > 0);

  • • 最后几句,恢复栈帧并返回

所以,第9-19行属于热点路径,也就是主循环体。第9-14行属于有效指令,第15-19行对于期望的数据结果来说就是无用指令。

我们看到,热点路径中,无用指令数占了整个热点路径指令数的一半,其开销也占到整个函数的50%!

无法充分发挥ILP技术优势

现代CPU为了提高指令执行的速度和吞吐率,提升系统性能,不仅一直致力于提升CPU的主频,还实现了多种ILP(Instruction-Level Parallelism 指令级并行)技术,如超流水线、超标量、乱序执行、推测执行、分支预测等。

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

但是,在代码热点路径上,无用指令太多,且每个迭代只执行一条*to = *from++,无法充分发挥ILP的技术优势。

注:这里解释不够清楚,详细讲解请参看文末推荐阅读的两篇文章,详细介绍了ILP技术(如超流水线、超标量、推测执行、分支预测)。

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

循环展开

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

循环展开,有以下优点:

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

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

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

注:关于循环展开的详细讲解,请参看文末推荐阅读的两篇文章(如果你还没看过的话)。

有童鞋可能会好奇,循环展开到底能提升多少性能呢?我们还是用数据说话,看一个实例吧。

实例 - 循环展开对性能的影响

测试环境:

OS:Ubuntu 19.04(Linux Kernel 5.0.0)
CPU:Intel(R) Xeon(R) Gold 6130
主频:2.10GHz
Cache 大小:22MB
Cache line 大小:64 Bytes

测试代码:

图片

loop1.c和loop2.c做的事情一样,唯一的区别是:

  • • loop1.c每次循环迭代执行一次k++

  • • loop2.c每次循环执行8次k++,但是循环的次数比loop1.c少了8倍

编译:

gcc loop1.c -o loop1
gcc loop2.c -o loop2

测试结果

图片

做同样的事情,通过循环展开优化,所消耗时间直接从25.4秒降到了14.7秒!

第一次优化尝试

了解了循环展开对性能提升的好处之后,我们就可以对上面的简单实现进行第一次优化尝试了。

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

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

图片

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

但是,我们前面做了一个假设,就是count是8的倍数。那如果不是8的整数倍呢,比如20?那我们可能会想到这样的实现:

图片

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

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

C语言switch-case的一些特性

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

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

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

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

图片

解释下这段代码:

我们假设count = 20,那么:

n = (count + 7) / 8 = 27 / 8 = 3
count % 8 = 4

所以:

  1. 1. switch语句会落入case 4的标签内,然后依次执行了case 4、3、2、1四条语句。自此之后,其实就跟switch-case语句再也没有关系了。

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

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

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

好了,到这里,大家应该理解Duff's Device了吧?还是不清楚的话,可以尝试单步跟踪一下,就会很清晰了。

揭晓答案

理解了Duff's Device之后,文章开头的那个题目就很好理解了,现在揭晓答案:

再看一下源码:

图片

编译运行:

图片

所以,答案是:20

友情提示(免责声明)

最后,友情提示:Duff's Device虽然精妙,但可读性确实较差,理解起来比较困难。因此,了解即可,产品代码中,不建议使用!否则,轻则被群殴,重则被祭天!

达夫设备(Duff‘s Device)浅析 达夫设备的核心优化思想是循环展开,通过减少循环迭代的次数来减少循环控制的开销,从而提高代码的执行效率。然而,这种技巧牺牲了代码的可读性,很容易导致代码维护上的困难,并且现代编译器的优化通常能够自动进行循环展开,所以它现在更多的是一个编程领域的趣闻,而不是推荐使用的代码实践。这段代码做的事情是将from指向的内存区域中的数据拷贝到to指向的内存区域中,拷贝的长度为count个字节。switch语句跳转到对应的case标签。这里的case标签作用是标记接下来需要执行的数据复制操作的起始点。 阅读详情

相关推荐

【C语言】常见的五个C语言基础代码示例

下面是一些有趣的C语言代码示例,展示了C语言的一些基本特性和技巧,可以帮助您更好地理解和掌握C语言的基础知识。

若北辰 5557

2023编程必会的100个代码大全,建议立刻收藏

编程必会的100个代码大全,建议收藏

2201_75571291的博客 6万+

Android开发者必看:火山引擎API验签实战,5分钟搞定年龄变化接口调用

本文为Android开发者提供火山引擎API验签实战指南,重点解析HMAC-SHA256签名算法实现,并以年龄变化接口为例展示完整调用流程。通过OkHttp封装和性能优化建议,帮助开发者5分钟内快速集成火山引擎人像处理功能,解决API调用中的签名验证、参数编码等核心问题。

weixin_30342209的博客 319

C语言项目源代码大全2023最新!

一、C语言源码大全 C语言是一门通用计算机编程语言,广泛应用于底层开发。C语言的设计目标是提供一种能以简易的方式编译、处理低级存储器、产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言。尽管C语言提供了许多低级处理的功能,但仍然保持着良好跨平台的特性,以一个标准规格写出的C语言程序可在许多电脑平台上进行编译,甚至包含一些嵌入式处理器(单片机或称MCU)以及超级电脑等作业平台。 博主已经把大量C语言源码整理为一个压缩包关注 微 信 公 众 号:C和C加加 回复:“源码”即可获取 二、C语言

chengxuyuanlaow的博客 4万+

Cesium实战:全球海洋动态水面与自定义区域波浪效果实现

本文详细介绍了在Cesium中实现全球海洋动态水面与自定义区域波浪效果的实战方法。通过解析核心概念Primitive与Geometry,并深入讲解Water材质的关键参数调节,指导开发者快速创建逼真的动态海洋。文章重点阐述了如何利用多边形坐标定义不规则图形区域,实现精准的局部水域波浪效果,并提供了性能优化与视觉提升的实用技巧。

weixin_29256771的博客 501

计算机组成与系统结构(第三版) 袁春风主编 课后习题答案讲解 第一、二章 计算机系统概述 数据的机器级表示

计算机组成与系统结构(第三版) 袁春风主编 课后习题答案讲解 第一、二章 计算机系统概述 数据的机器级表示

qq_73523062的博客 2096

20行c语言代码案例,精通C语言?短短20行经典C语言代码很多人看不明白,你来试一下吧...

对编译、链接、OS内核、性能优化等技术感兴趣的童鞋,不妨右上角关注一下吧,近期会持续更新相关方面的专题文章!引言昨天发了一个文章《简历上写精通C语言?有道C语言的题来做一下吧》,引来很多童鞋围观。很多童鞋表示不太明白,于是就有了这篇文章,详细解释下这个题目的来龙去脉。题目如下图所示(对原题目做了少许改动):test.c如果你是第一次看到的话,不妨试一下,看你能得出正确答案吗?其实,这个题目还是源自...

weixin_42513269的博客 754

STM32Cubemx-H7-17-麦克纳姆轮驱动

我们先看.h,知道哪些函数会有哪些功能和宏定义参数首先 是在.h的宏定义处定义引脚编号,比如tb6612控制方向的引脚,以及设定了哪个定时器作为pwm输出,还有编码计次的中断引脚/* 硬件相关宏定义 *//*1 前进方向往上 42 3*/// TB6612 方向引脚定义/* 定时器相关宏定义 *//* 编码器相关宏定义 */把自己的引脚根据原理图填对应,方向引脚也是,如果发现反转就把对应的引脚定义反过来,编码也是,编码我们用一个引脚就好,都在cubemx配置好。

m0_74211379的博客 1423

60多行的c语言程序,精通C语言?短短20行经典C语言代码很多人看不明白,你来试一下吧...

对编译、链接、OS内核、性能优化等技术感兴趣的童鞋,不妨右上角关注一下吧,近期会持续更新相关方面的专题文章!引言昨天发了一个文章《简历上写精通C语言?有道C语言的题来做一下吧》,引来很多童鞋围观。很多童鞋表示不太明白,于是就有了这篇文章,详细解释下这个题目的来龙去脉。题目如下图所示(对原题目做了少许改动):test.c如果你是第一次看到的话,不妨试一下,看你能得出正确答案吗?其实,这个题目还是源自...

weixin_35988038的博客 347

产品路线图的三种模型(RICE/MoSCoW/Kano),教你如何对需求进行优先级排序

RICE/MoSCoW/Kano三种模型,教你如何对需求进行优先级排序。 也许你会让业务提供一份ROI,记得前不久参加过一场立项会评审,一位很有意思的小产品在会上讲不清楚ROI,评审们要求会后重新梳理,小产品表达不要为难业务方干一些他们不擅长的事了。这个看似笑话的经历,背后隐藏着价值判断已经流于形式。 也许你会要求业务方直接给出需求优先级列表,把任务分为P0到P5,这样的划分除了按需求本质的优先级还同时考虑了开发资源、开发难度等问题。毫不夸张地说,我曾经拿到一张全是P0级别的需求清单,如果一切任务都是高

wenyusuran的专栏 5545

华为OD机考真题 新系统 - 小菊的寻宝之旅(Java/Py/C/C++/Js/Go)

华为OD机试真题 新系统【小菊的寻宝之旅】,提供C语言、Java、C++、Python、Go、JavaScript源码实现,并提供完整思路讲解,以及在线OJ模拟做题。华为OD机试真题 【小菊的寻宝之旅】多语言题解。

qq_45776114的博客 627

Duff‘s Device,“达夫设备“,提高C代码运行效率

2.运行循环展开以及switch特性:case可以出现在if、for、while中(有点goto的感觉)博客摘录微信公众号原点科技文章:《20行经典C代码,很多人看不明白,你来试一下?两段代码都是同样的效果,遍历相同的次数,效率提升明显。将from中的指定长度复制到to中。运行时间:3418ms。

pia_pia_pia_的博客 552

Megatron-LM从头训练大语言模型:并行策略图示【Tensor并行、Pipeline并行、Data并行、Sequence并行、virtual_pipeline并行、Context并行】

节点包含的层每GPU的参数内存每GPU的激活内存节点0Stage 0层1-1212层参数/2 (TP切分)8k序列激活 (CP切分)节点1Stage 1层13-2412层参数/2 (TP切分)8k序列激活 (CP切分)节点2Stage 2层25-3612层参数/2 (TP切分)8k序列激活 (CP切分)节点3Stage 3层37-4812层参数/2 (TP切分)8k序列激活 (CP切分)负载均衡:每个节点处理相同数量的层(12层)内存优化。

u013250861的博客 285

想成为更好的程序员吗?这些编程时应该注意的点,不妨来试一试

简单就好 我们喜欢创建一些新事物、解决一些疑难杂症。这也是为什么我们干这一行的原因。但是很多时候,我们发现某个问题可以解决,却并不意味着现在就是解决它的好时机。 理解是重点,要知道程序员大部分时间是在维护代码,而不是写代码。 不要仅仅修复 bug;要修复所有可能发生 bug 的地方 对于自己犯的错误,没必要耿耿于怀。每个人都讨厌出现 bug,我也是。 我讨厌会让我犯错的系统。而且我真的非常非常讨厌去修复同样的 bug,所以为了避免这种...

C语言C++学习俱乐部:765860056 338

Codesys入门-新建工程+任务调用+编译+调试

程序组织单元(Program Organization Unit, POU)由声明区和代码区组成,是用户程序的最小软件单元,它相当于传统编程系统中的块(Block),按功能程序组织单元(POU)可分为函数(FUN)、功能块(FB)、程序(PRG)。:Codesys采用多任务的方式来执行用户程序的“任务”,而每个“任务”分配了不同的执行周期,有些全局变量可能要在不同的POU之间被访问和修改,于是需要对全局变量进行交互同步,也是在任务的“时间对准点”进行的,实例:声明了INT类型,变量名为输入;

刺客 1万+

锂电池充电器电路设计 里面有电路

锂电池充电器电路设计 里面有电路 3.7V升压到5V电路设计。

命令点亮硬盘灯_搞不懂NAS,来试一试硬盘柜吧——奥睿科DS硬盘柜

前言大家好,我是你们特特,又见面了。之前给大家写过一篇关于NAS入门玩法的文章,我感觉自己讲的还是很详细了,但还有很多朋友抱怨玩NAS需要的学习成本太高了,他们很多时候都搞不明白到底该如何设置,甚至有朋友买了NAS又把它给卖掉了,就是因为搞不懂到底怎么才能把NAS玩出花样,或是他们根本就只想储存个东西罢了。那今天我就给大家介绍个即插即用的硬盘柜,对于绝大多数只有储存需求的人来说相当合适。上手最简单...

weixin_39903375的博客 581

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

字节通常用英文单词bit来表示_又是一个看似简单但易错的C语言试题,来试一下?...

今天看到了一个比较有意思的C语言题目,看似简单,但里面的几个陷阱和考察的知识点比较有代表性,拿出来与大家分享一下:请输入图片描述问题很简单,就问会输出什么?大家可以先自己思考一下,我们就直接给出运行结果了:请输入图片描述是不是很奇怪,怎么会出现了255,-1是没问题的。别急,下面我们一步一步分析:先来分析a和b的值:我们首先要明白的是,在计算机中整数通常采取补码的形式存储。负数的补码等于其反码+...

weixin_33188832的博客 218

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

递推公式求5的阶乘c语言代码,作为程序员的你必备的神仙算法

递归是一种非常重要的算法思想,无论你是前端开发,还是后端开发,都需要掌握它。在日常工作中,统计文件夹大小,解析xml文件等等,都需要用到递归算法。它太基础太重要了,这也是为什么面试的时候,面试官经常让我们手写递归算法。本文呢,将跟大家一起深入挖掘一下递归算法~什么是递归?递归,在计算机科学中是指一种通过重复将问题分解为同类的子问题而解决问题的方法。简单来说,递归表现为函数调用函数本身。在知乎看到一...

weixin_30814737的博客 2013

lisp编译器_我敢打赌,没几个人知道Lisp...

# 简介最初在web的某些角落偶然看到有人赞美Lisp时, 我那时已经是一个颇有经验的程序员。在我的履历上, 掌握的语言范围相当广泛, 象C++, Java, C#主流语言等等都不在话下, 我觉得我差不多知道所有的有关编程语言的事情。对待编程语言的问题上, 我觉得自己不太会遇到什么大问题。其实我大错特错了。我试着学了一下Lisp, 结果马上就撞了墙。我被那些范例代码吓坏了。我想很多初次接触Lisp...

weixin_39976575的博客 2575

c语言基础3

1.但大家看到代码中出现的//是否也会感到疑惑,其实这就是c语言中的注释形式,这是其中一种,还有一种为*......*的形式。3. \0 :null 字符,代表没有内容, \0 就是 \ddd 这类转义字符的⼀种,⽤于字符串的结束标志,其。11. \v :垂直分隔符,光标移到下⼀个垂直制表位,通常是下⼀⾏的同⼀列。10. \t :制表符,光标移到下⼀个⽔平制表位,通常是下⼀个8的倍数。4. \\ :⽤于表⽰⼀个反斜杠,防⽌它被解释为⼀个转义序列符。6.\b :退格键,光标回退⼀个字符,但不删除字符。

c23856的博客 635

Xuan的C语言学习之旅——(1)Hello World

第一天让我们学点轻松的,了解一下C语言

m0_54852529的博客 319

【C进阶】——内存操作函数memcpy、memmove、memcmp、memset详解及其模拟实现

1.内存块拷贝函数——memcpy1.1 函数介绍1.2 memcpy的模拟实现1.3 思考2.移动内存块(可拷贝重叠内存块)——memmove2.1 函数介绍2.2 memmove的模拟实现3. Visual Studio 对memcpy的实现4. 内存比较函数—— memcmp4.1函数介绍4.2 memcmp的模拟实现5. 内存设置函数—— memset5.1函数介绍5.2 memset的模拟实现

m0_70980326的博客 7570

scanf函数的用法_ios从入门到放弃之C基础巩固printf函数、scanf函数、scanf函数实现原理...

printf函数:继续IOS的C基础之旅,对于printf函数来说应该都非常之熟了,除了C有这个函数外,像Koltin也是有它,这里还是对它进行一个梳理,它里面的有些细节如果未来忘了回来也可以查询一下,很简单但是有必要把它的一些值得记录的细节给记录下来,看下面。域宽问题:下面来实验一下:上面代码没啥好说的,数字本身只有2个默认输出也就是实际数字的长度,但是如有需求需要输出固定为5的数字长...

weixin_39669982的博客 188

C语言指针

讲述了C语言指针的使用使用时可能会出现的错误

qwer456tyu123的博客 1887
上一篇: 想解决BUG,但不想修改代码,更不想重新编译!怎么办?
下一篇: 今天起,Windows可以一键召唤GPT-4了
IT界那些事儿
博客等级 码龄6年 2万+粉丝 22原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值