当前位置:首页 > 程序
  • 在学习中我们如何快速准确的读懂单片机程序?

    在学习中我们如何快速准确的读懂单片机程序?

    在现实中,我相信有很多刚刚接触单片机的同学,简直是无从下手,打开一个程序,更会被复杂的结构和密密麻麻的代码吓到,产生退缩的想法,这篇文章带你了解一下单片机程序。 我对单片机的总结:“单片机其实就是一个芯片,内部有若干寄存器,外部有若干引脚,我们可以通过程序控制内部的寄存器使得引脚与外部世界保持联系!”就这几句话,道出了单片机的真谛!有没有感觉到单片机是多么的简单! 1.单片机程序执行流程 这是我们首先必须要知道的。单片机程序一般就有两种,一种是汇编程序,一种是c语言程序。这里我们讲c语言程序。 单片机程序都有一个包含主函数的文件,包含主函数的文件都有一个统一的结构,如下所示: #include "xxx.h" int main() // 这是主函数的函数名 { ......; // 若干条语句 ......; while(1) // while括号中是1,说明程序进入后将在while里面无线循环,不会出来了,不懂的去看c语言基础之while篇 { ......; // 若干条语句 ......; } } 重点:单片机一上电,从主函数main的第一条语句开始执行,是一条语句接着一条语句从上而下执行,直到进入while后,再从while的第一条语句执行到最后一条语句,由于是死循环,会再从while的第一条语句执行到最后一条语句,如此反复执行,永不停止!直到断电! 这些语句当中,有些是函数的调用,遇到函数的调用,进入到函数,再从函数的第一条语句执行到最后一条语句,然后跳出函数,再从刚才主函数中那条函数的下一条语句开始执行。如果实在搞不明白函数是怎么一回事,你可以用函数里面的所有语句代替函数在主函数中的位置。例如: #include "LPC11XX.H" #define LED1_ON LPC_GPIO1->DATA &= ~(1<<0) #define LED1_OFF LPC_GPIO1->DATA |= (1<<0) #define LED2_ON LPC_GPIO1->DATA &= ~(1<<1) #define LED2_OFF LPC_GPIO1->DATA |= (1<<1) /***********************************/ /* 延时函数 */ /***********************************/ void delay() { uint16_t i,j; for(i=0;i<5000;i++) for(j=0;j<200;j++); } /***********************************/ /* LED初始化函数 */ /***********************************/ void led_init() { LPC_SYSCON->SYSAHBCLKCTRL |= (1<<16); LPC_IOCON->R_PIO1_0 &= ~0x07; LPC_IOCON->R_PIO1_0 |= 0x01; LPC_IOCON->R_PIO1_1 &= ~0x07; LPC_IOCON->R_PIO1_1 |= 0x01; LPC_SYSCON->SYSAHBCLKCTRL &= ~(1<<16); LPC_GPIO1->DIR |= (1<<0); LPC_GPIO1->DATA |= (1<<0); LPC_GPIO1->DIR |= (1<<1); LPC_GPIO1->DATA |= (1<<1); } /***********************************/ /* 主函数 */ /***********************************/ int main() { led_init(); while(1) { delay(); LED1_ON; LED2_OFF; delay(); LED1_OFF; LED2_ON; } } 上面这个例子中,单片机一上电,会执行主函数的第一条语句,也就是led_init(),这个是一个函数的调用语句,程序会从led_init函数中的第一条语句开始执行,直到执行完最后一条语句后,回到主函数,进入while,从while的第一条语句delay()开始执行,delay()又是一个函数,程序会从delay()的第一条语句开始执行,delay()函数中有两个for循环,执行完for循环后,就跳出delay()函数,执行LED1_ON,由于LED1_ON是个用#define定义的宏定义,由c语言基础知识之#define宏定义篇,我们知道,LED1_ON就是LPC_GPIO1->DATA &= ~(1<<0),如此继续执行下去……。 如果不用define宏定义,也不用函数,上面的例子就可以写为如下形式: #include "LPC11XX.H" /***********************************/ /* 主函数 */ /***********************************/ int main() { //LED初始化 LPC_SYSCON->SYSAHBCLKCTRL |= (1<<16); LPC_IOCON->R_PIO1_0 &= ~0x07; LPC_IOCON->R_PIO1_0 |= 0x01; LPC_IOCON->R_PIO1_1 &= ~0x07; LPC_IOCON->R_PIO1_1 |= 0x01; LPC_SYSCON->SYSAHBCLKCTRL &= ~(1<<16); LPC_GPIO1->DIR |= (1<<0); LPC_GPIO1->DATA |= (1<<0); LPC_GPIO1->DIR |= (1<<1); LPC_GPIO1->DATA |= (1<<1); while(1) { for(i=0;i<5000;i++) for(j=0;j<200;j++); LPC_GPIO1->DATA &= ~(1<<0); LPC_GPIO1->DATA |= (1<<1); for(i=0;i<5000;i++) for(j=0;j<200;j++); LPC_GPIO1->DATA |= (1<<0); LPC_GPIO1->DATA &= ~(1<<1); } } 有没有发现,第二种表示方法,虽然不涉及函数和宏定义了,对于c语言掌握不是很好的人来说,看的比较爽。如果你掌握了c语言的这些宏定义和函数的小技巧,第一种表示方法是不是更有利于阅读程序的功能呢? 2.读懂程序需要c语言基础知识,当然,也可以边看程序,边学习c语言基础知识。 3.读懂程序需要会看单片机的寄存器定义,在程序中,大都是在给单片机的寄存器进行配置或是获取单片机寄存器的数据。看哪种单片机程序,就要学会看哪种单片机的寄存器定义。知道了寄存器的定义,就知道如何配置寄存器或是获取的寄存器数据代表的意义了。 例如我们要看LPC1114的程序,那么LPC1114的用户手册是必须要打开的。例如LPC_SYSCON->SYSAHBCLKCTRL |= (1<<16);这条语句,就是在给SYSCON模块中的SYSAHBCLKCTRL寄存器进行配置,所以我们要找到这个寄存器的定义。首先,打开用户手册,找到SYSCON这一章,然后找到寄存器描述这一节,就可以找到这个寄存器的定义了。至于(|=(1<<16))这些,都是写基本的逻辑运算,也是些c语言的基础知识而已。例如(|=(1<<16)) 这个就是把1左移16个位,然后把左移后的数据与SYSAHBCLKCTRL寄存器进行或运算,运算后的结果再放入SYSAHBCLKCTRL寄存器当中。1左移16个位,就是bit16为1,其它位为0。与寄存器SYSAHBCLKCTRL进行或运算,我们不管这个寄存器原来的值是多少,我们现在只知道,1或任何数,都等于1;0或任何数,都等于任何数。所以,1左移16位后,再与寄存器进行或运算,实际上是把寄存器的bit16置1,剩下的位原来是多少,还是多少。 总结一句话,学习单片机主要是把程序里面的“或”“和”“进制转换”搞清楚,就很容易搞懂单片机程序了。

    时间:2021-03-06 关键词: 单片机 程序

  • 来给你代码加上美颜吧!

    每个程序员只要不犯错,都能写出机器能看得懂的代码,程序能正常跑起来,自然就意味着机器正常识别了程序。 但是,真正牛逼的程序员是写出能让人看得懂的代码。 不要小看这个,虽说我们写的代码确实是跑给机器的,但是代码是人写的,而通常一个项目的开发,需要多个程序员一同协助开发,这时能写出 human readble 的代码就显得至关重要,因为不仅可以减少后期维护的时间成本,而且还能让后面加入的新同事能更快的上手项目。 要能写出干净、整洁并让人易懂的代码,必然离不开一些规则,只要自觉遵守、合理运用这些规则,代码通常都不会太差。 事实上,每个公司或者开源项目都有各自的编码风格指南文档,这些文档无不例外都罗列了十几个、上百个的条款,基本上都是干干巴巴的示例和说明,不说能不能看完, 即使看完了,也都忘了差不多,非常容易被劝退。 这次,我就从中抽离几个重要的条款,以及结合我工作的经验,把写出好的 code style 的几个注意事项跟大家说下。 只要注意代码格式、变量命名和注释三个方面,代码的“颜值”起码提高 80%。 往大的说,能写出“高颜值”的代码的人,也能从这个小事反映出他是一个细心的人,同时也具备责任感,只要把每一件小事做好、做到极致,渐渐的,你的同事和上级就会对你产生信任感。 代码格式 第一眼看代码就是看代码的整体格式,好的代码格式,一眼就能让人感到清爽、舒服,我们本身每天工作就比较繁忙了,还要面对乱糟糟的代码格式,心情肯定差到极点,感觉像是吃了一坨 shi。 文章也是一样,小林我也很注重文章的排版,我的排版也被很多读者夸赞过,不用追求花里胡哨的装饰,只要保持简洁、大方就行,虽说文章是我写的,但是文章是给别人看的,那我肯定要为读者的“眼睛”负责呀。 一个好的代码格式,只需要遵循五个字,那就是「留白的艺术」。 代码和文章,不要为了节约篇幅,把一大坨东西“挤”在一起,这样只会给人家带来压迫感。 多运用空格和换行,用空格分隔开变量与操作符,用空行分割开代码块。 说了那么多口水话,接下来上点实际代码例子。 来,我上一大坨的代码给大家看看: 是不是很密集?看的很难受?压迫感++ 是不? 什么?你说你没感觉,那你被我逮到了,你肯定是经常写这类车祸现场代码的凶手,搞崩团队心态的发动机。 运用好「留白的艺术」,代码就变成下面这样: 是吧,只需简单运用空格和空行,代码就显得很清爽,段落层次分明,读起来不会太累,也更加容易理清代码的逻辑。 所以,敲代码的同时,别忘了用空格和空行“装饰”下你的代码,你的每一处留白,都是在拯救别人的眼睛。 变量命名 不知道你是不是写过变量名为,a、b、c、d...的代码,如果代码只是你测试用的,那也问题不大,但是我不信在你把代码越写越多后,还记得这些变量的作用。同样,也不要在一个多人维护的项目里,干出这样的事情,你十有八九会被同事孤立。 给变量命名是有讲究的,不是随意的,取的名字应该是让人知道该变量的作用,减少大家根据上下文才猜测的时间。 命名最好以英语的方式描述,而不要汉语拼音,英语词汇不过关也没事,敲代码本身就是一个“开卷考试”的事情,打开浏览器,随意都能在各种翻译网站找到合适的英语词汇。 另外,有一些变量名在程序员之间已经形成了普遍共识,这些都是可以直接使用的,比如: 用于循环的 i/j/k; 用于计数的 count; 表示指针的 p/ptr; 表示缓冲区的 buf/buffer; 表示总和的 sum; … 对于变量命名的风格,一般应用比较广泛的有三种。 第一种风格叫「匈牙利命名法」,早期是由微软的一个匈牙利人发明的,当时 IDE 并没有那么智能,识别不出变量的类型,代码量一大起来,要确定一个变量的类型是个麻烦的事情,于是就要求使用变量类型的缩写作为变量名的前缀字母,代码例子如下图: 但是这个风格在面临代码重构时,就是一个灾难了,因为如果更换了变量的类型,那么还得把变量名也全部改过来,所以这种风格基本被淘汰了。 不过它里面还有一种做法还是不错的,对于有作用域的变量会加上对应的前缀来标识,给成员变量加 m_前缀,比如 m_count,给全局变量加 g_前缀,比如 g_total,这样一看到前缀就知道变量的作用域了,很清晰明了。 第二种风格叫「驼峰式命名法」,主张单词首字母大写,从而形成驼峰式的视觉,对于变量的首字母有的要求是小写,有的要求是大写,比如 myName、MyAge。这种风格在 Java 语言非常流行,但在 C/C++ 语言里用的比较少。 第三种风格叫「下划线命名法」,变量名用的都是小写,单词之间用下划线分割开,比如 my_name、my_age,这种风格流行于 C/C++ 语言。 这些风格也不是说只能固定只能用一种,我们可以结合一起使用的,我常用的语言是 C/C++,我对自己一般有以下这几个规则: 变量名、函数名用下划线命名风格,对于有作用域的变量,也会使用前缀字母来标识,比如成员变量用m_、全局变量用 g_、静态变量用 s_; 类的名字用驼峰式命名风格,比如 VideoEncode、FilePath; 宏和常量用全大写,并用下划线分割单词,比如 MY_PATH_LEN; 下面用实际代码作为例子: 关于变量命名还有一个问题是,名字的长度如何才是合理的。 有一个普遍被大家认可的原则是:名字越长,它的作用域应越广。也就是说,局部变量的名字可以短一些,全局变量的名字可以长一些。 注释 留白的艺术用上,变量名规范也用上,让每个人都能一眼看懂的代码,还差点东西,那就是注释。 注释存在的原因就是为了让人快速理解代码的逻辑,一个好的注释,是能让人只看注释就知道代码的意图。 我猜大家注释也写了不少,注释一般是用来阐述目的、用途、工作原理、注意事项等等,注释必须要正确、清晰、言简意赅,而且在修改代码逻辑时,也别忘记了要更正注释,否则就会误导人了。 注释最好写明作者、时间、用途、注意事项这四个信息,比如下面这个例子: 注释用中文好还是英文好呢? 当然是英文比较好,因为英文在 ASCII 或 UTF-8 编码格式里兼容性很好,而中文可能会在导致在一些编码格式里显示乱码,乱码了就自然失去注释的作用了。 注释最好也要统一使用一个标准格式,比如 Java 语言一般是使用 Javadoc 注释标准,遵循该标准后,会有专门的工具可以一键生成 API 文档。 当然,除了给代码、函数、类写好注释,文件顶部位置也可以写上对该文件的注释,信息一般是版权声明、更新历史、功能描述等。 比如下面这个,是比较常用注释文件的一种标准: 再来,如果你发现某个地方的代码有改进或未实现的地方,而暂时没有时间去修改的时候,可以使用 TODO来标识它,这样代码需要优化的时候,直接搜索这个关键词就可以了,也可以给将来的维护者一个提醒。比如: 注释要写的好,得要有换位思考的思维,想着写怎么样注释能让那些没有参与过该项目开发的人能最快速的理解,以便能加快他们融入该项目的速度。 总结 要写出高颜值的代码,离不开良好的编程习惯,今天主要提了三个重要点: 留白艺术的妙处,多运用空格和空行; 变量名、函数名、类名要起个让人容易理解的名字; 注释要写好,多换位思考,最好也要遵循一些注释标准,便于自动生成 API 文档; 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2021-02-22 关键词: 代码 程序员 程序

  • Qt原来开发出了这么多的产品

    什么是qt? 简单点说,Qt 就是一个跨平台的 C++ 图形用户界面库,可以同时支持桌面应用程序开发、嵌入式开发和移动开发,覆盖了现有的所有主流平台。 可以做什么 使用 Qt 开发的程序非常多。自从1997年Qt被用来开发Linux桌面环境KDE大获成功开始以来,QT就成为了Linux 环境下开发 C++ GUI 程序的事实标准。 虽然在Windows下,GUI解决方案比较多,基于C++的有Qt、MFC、WTL、wxWidgets、DirectUI、Htmlayout等等,基于C#的有 WinForm、WPF等等,基于Java的有AWT、Swing等等,但是qt依然占据了很大部分。 在消费类电子、工业控制、军工电子、电信/网络/通讯、航空航天、汽车电子、医疗设备、仪器仪表等相关行业,也都有 Qt 的影子。 最近因为写自动化测试软件,开始搞pyqt,用python、Qt联合开发,发现原来qt还开发了这么多的产品。 这其中不乏YY语音,咪咕音乐,WPS Office,虾米音乐,Google地图等等我们耳熟能详的应用。 Qt有什么特点优点? 简单易学 Qt 封装的很好,少量代码就可以开发出一个简单的客户端,他的宗旨也是 code less , crate more 。 面向对象 良好封装机制使得Qt的模块化程度非常高,可重用性较好,便于移植。这一点对于用户开发来说是非常方便高效的。 并且Qt提供了一种称为信号与槽signals/slots的安全类型来替代callback,这使得各个功能模块之间的协同工作变得十分简单,也很容易理解。 大量的开发文档 前些年资料还是很少的,但是随着Qt的发展以及越来越多的开发者,资料也越来越丰富了,这些都能够成倍降低学习成本。 漂亮的界面 Qt 很容易做出漂亮的界面和炫酷的动画,并且支持 2D/3D 图形渲染,支持 OpenGL,而 MFC、WTL、wxWidgets 比较麻烦。 独立安装 Qt 程序最终会编译为本地代码,不需要其他库的支撑,而Java要安装虚拟机,C#要安装 .NET Framework。 优良的跨平台特性 如果你的程序需要运行在多个平台下,同时又希望降低开发成本,Qt 几乎是必备的。qt的开发方式可以参考Qt值得学习吗?详解Qt的几种开发方式 丰富的 API Qt包括几百个C++类,还提供基于模板的file,I/O device,directory management,collections,serialization,date/time 类等等。 最后 如果用户使用 C++,并且对库的稳定性,健壮性要求比较高,并且希望跨平台开发的话,那么使用 Qt 是较好的选择。 END 来源:技术让梦想更伟大, 作者:李肖遥 版权归原作者所有,如有侵权,请联系删除。 ▍ 推荐阅读 树莓派Pico:仅4美元的MCU 嵌入式Linux开发板裸机程序烧写方法总结 国产16位MCU的痛点,可以用这款物美价廉产品 →点关注,不迷路← 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2021-02-20 关键词: 开发 QT 程序

  • 如何掌握“所有”的程序语言?没错,就是所有!

    -END- 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2021-01-22 关键词: 嵌入式 语言 程序

  • 如何掌握“所有”的程序语言???

    大家好,我是小林。 最近我的蓝牙键盘电池没电了,电池用的挺快的,不到几个月就没电了,估计是因为一直 24 小时开着的原因吧… 没有了键盘,我的姿势就成了这样。。 最佳姿势,CV 大法好,有木有! 所以,文章你懂得哈哈哈,正在写…… 所以给大家分享一篇川大学长王垠写的一篇文章: 《如何掌握所有的程序语言》 我刚看完,文章很不错,作者站的高度很高,固然我们一般人很难达到。 但是这篇文章对于指导初学者该如何选编程语言,以及学习编程语言的正确方式还是很具有指导意义的。 如果你还不了解这位备受争议的大佬,我在这里放一个他的简介: 王垠,四川大学97级本科毕业,保送到清华大学计算机系直博。期间曾在清华大学计算机系软件工程专业就读,主要进行集成电路布线算法的研究。在此期间,他因《完全用GNU/Linux工作》一文和对 TeX 的推广等“非研究成果的业余东西”而出名。 在只剩一年就要博士毕业的时候,他申请退学,并将1万7千余字的“退学申请书”(题为清华梦的粉碎)公布在网上,引起舆论界一时对教育体制、理想主义等的热议。 文章有点长,耐心看下来,相信还是有收获的,下面是正文。 正文 对的,我这里要讲的不是如何掌握一种程序语言,而是所有的…… 很多编程初学者至今还在给我写信请教,问我该学习什么程序语言,怎么学习。 由于我知道如何掌握“所有”的程序语言,总是感觉这种该学“一种”什么语言的问题比较低级,所以一直没来得及回复他们 。 可是逐渐的,我发现原来不只是小白们有这个问题,就连美国大公司的很多资深工程师,其实也没搞明白。 今天我有动力了,想来统一回答一下这个搁置已久的“初级问题”。类似的话题貌似曾经写过,然而现在我想把它重新写一遍。 因为在跟很多人交流之后,我对自己头脑中的(未转化为语言的)想法,有了更精准的表达。 如果你存在以下的种种困惑,那么这篇文章也许会对你有所帮助: 你是编程初学者,不知道该选择什么程序语言来入门。 你是资深的程序员或者团队领导,对新出现的种种语言感到困惑,不知道该“投资”哪种语言。 你的团队为使用哪种程序语言争论不休,发生各种宗教斗争。 你追逐潮流采用了某种时髦的语言,结果两个月之后发现深陷泥潭,痛苦不堪…… 虽然我已经不再过问这些世事,然而无可置疑的现实是,程序语言仍然是很重要的话题,这个情况短时间内不会改变。 程序员的岗位往往会要求熟悉某些语言,甚至某些奇葩的公司要求你“深入理解 OOP 或者 FP 设计模式”。对于在职的程序员,程序语言至今仍然是可以争得面红耳赤的宗教话题。 它的宗教性之强,以至于我在批评和调侃某些语言(比如 Go 语言)的时候,有些人会本能地以为我是另外一种语言(比如 Java)的粉丝。 显然我不可能是任何一种语言的粉丝,我甚至不是 Yin 语言的粉丝,对于任何从没见过的语言,我都是直接拿起来就用,而不需要经过学习的过程。 看了这篇文章,也许你会明白我为什么可以达到这个效果。 理解了这里面的东西,每个程序员都应该可以做到这一点。 嗯,但愿吧。 重视语言特性,而不是语言 很多人在乎自己或者别人是否“会”某种语言,对“发明”了某种语言的人倍加崇拜,为各种语言的孰优孰劣争得面红耳赤。 这些问题对于我来说都是不存在的。 虽然我写文章批评过不少语言的缺陷,在实际工作中我却很少跟人争论这些。 如果有其它人在我身边争论,我甚至会戴上耳机,都懒得听他们说什么。 为什么呢? 我发现归根结底的原因,是因为我重视的是“语言特性”,而不是整个的“语言”。 我能用任何语言写出不错的代码,就算再糟糕的语言也差不了多少。 任何一种“语言”,都是各种“语言特性”的组合。 打个比方吧,一个程序语言就像一台电脑。 它的牌子可能叫“联想”,或者“IBM”,或者“Dell”,或者“苹果”。 那么,你可以说苹果一定比 IBM 好吗? 你不能。 你得看看它里面装的是什么型号的处理器,有多少个核,主频多少,有多少 L1 cache,L2 cache……,有多少内存和硬盘,显示器分辨率有多大,显卡是什么 GPU,网卡速度,等等各种“配置”。 有时候你还得看各个组件之间的兼容性。这些配置对应到程序语言里面,就是所谓“语言特性”。 举一些语言特性的例子: 变量定义 算术运算 for 循环语句,while 循环语句 函数定义,函数调用 递归 静态类型系统 类型推导 lambda 函数 面向对象 垃圾回收 指针算术 goto 语句 这些语言特性,就像你在选择一台电脑的时候,看它里面是什么配置。 选电脑的时候,没有人会说 Dell 一定是最好的,他们只会说这个型号里面装的是 Intel 的 i7 处理器,这个比 i5 的好,DDR3 的内存 比 DDR2 的快这么多,SSD 比磁盘快很多,ATI 的显卡是垃圾…… 如此等等。 程序语言也是一样的道理。 对于初学者来说,其实没必要纠结到底要先学哪一种语言,再学哪一种。 曾经有人给我发信问这种问题,纠结了好几个星期,结果一个语言都还没开始学。 有这纠结的时间,其实都可以把他纠结过的语言全部掌握了。 初学者往往不理解,每一种语言里面必然有一套“通用”的特性。比如变量,函数,整数和浮点数运算,等等。 这些是每个通用程序语言里面都必须有的,一个都不能少。 你只要通过“某种语言”学会了这些特性,掌握这些特性的根本概念,就能随时把这些知识应用到任何其它语言。 你为此投入的时间基本不会浪费。 所以初学者纠结要“先学哪种语言”,这种时间花的很不值得,还不如随便挑一个语言,跳进去。 如果你不能用一种语言里面的基本特性写出好的代码,那你换成另外一种语言也无济于事。你会写出一样差的代码。 我经常看到有些人 Java 代码写得相当乱,相当糟糕,却骂 Java 不好,雄心勃勃要换用 Go 语言。 这些人没有明白,是否能写出好的代码在于人,而不在于语言。 如果你的心中没有清晰简单的思维模型,你用任何语言表述出来都是一堆乱麻。 如果你 Java 代码写得很糟糕,那么你写 Go 语言代码也会一样糟糕,甚至更差。 很多初学者不了解,一个高明的程序员如果开始用一种新的程序语言,他往往不是去看这个语言的大部头手册或者书籍,而是先有一个需要解决的问题。 手头有了问题,他可以用两分钟浏览一下这语言的手册,看看这语言大概长什么样。 然后,他直接拿起一段例子代码来开始修改捣鼓,想法把这代码改成自己正想解决的问题。 在这个简短的过程中,他很快的掌握了这个语言,并用它表达出心里的想法。 在这个过程中,随着需求的出现,他可能会问这样的问题: 这个语言的“变量定义”是什么语法,需要“声明类型”吗,还是可以用“类型推导”? 它的“类型”是什么语法?是否支持“泛型”?泛型的 “variance” 如何表达? 这个语言的“函数”是什么语法,“函数调用”是什么语法,可否使用“缺省参数”? …… 注意到了吗?上面每一个引号里面的内容,都是一种语言特性(或者叫概念)。 这些概念可以存在于任何的语言里面,虽然语法可能不一样,它们的本质都是一样的。 比如,有些语言的参数类型写在变量前面,有些写在后面,有些中间隔了一个冒号,有些没有。 这些实际问题都是随着写实际的代码,解决手头的问题,自然而然带出来的,而不是一开头就抱着语言手册看得仔仔细细。 因为掌握了语言特性的人都知道,自己需要的特性,在任何语言里面一定有对应的表达方式。 如果没有直接的方式表达,那么一定有某种“绕过方式”。 如果有直接的表达方式,那么它只是语法稍微有所不同而已。 所以,他是带着问题找特性,就像查字典一样,而不是被淹没于大部头的手册里面,昏昏欲睡一个月才开始写代码。 掌握了通用的语言特性,剩下的就只剩某些语言“特有”的特性了。 研究语言的人都知道,要设计出新的,好的,无害的特性,是非常困难的。 所以一般说来,一种好的语言,它所特有的新特性,终究不会超过一两种。 如果有个语言号称自己有超过 5 种新特性,那你就得小心了,因为它们带来的和可能不是优势,而是灾难! 同样的道理,最好的语言研究者,往往不是某种语言的设计者,而是某种关键语言特性的设计者(或者支持者)。 举个例子,著名的计算机科学家 Dijkstra 就是“递归”的强烈支持者。现在的语言里面都有递归,然而你可能不知道,早期的程序语言是不支持递归的。 直到 Dijkstra 强烈要求 Algol 60 委员会加入对递归的支持,这个局面才改变了。Tony Hoare 也是语言特性设计者。 他设计了几个重要的语言特性,却没有设计过任何语言。另外大家不要忘了,有个语言专家叫王垠,他是早期 union type 的支持者和实现者,也是 checked exception 特性的支持者,他在自己的博文里指出了 checked exception 和 union type 之间的关系 :P 很多人盲目的崇拜语言设计者,只要听到有人设计(或者美其民曰“发明”)了一个语言,就热血沸腾,佩服的五体投地。 他们却没有理解,其实所有的程序语言,不过是像 Dell,联想一样的“组装机”。 语言特性的设计者,才是像 Intel,AMD,ARM,Qualcomm 那样核心技术的创造者。 合理的入门语言 所以初学者要想事半功倍,就应该从一种“合理”的,没有明显严重问题的语言出发,掌握最关键的语言特性,然后由此把这些概念应用到其它语言。 哪些是合理的入门语言呢?我个人觉得这些语言都可以用来入门: Scheme C Java Python JavaScript 那么相比之下,我不推荐用哪些语言入门呢? Shell PowerShell AWK Perl PHP Basic Go Rust 总的说来,你不应该使用所谓“脚本语言”作为入门语言,特别是那些源于早期 Unix 系统的脚本语言工具。 PowerShell 虽然比 Unix 的 Shell 有所进步,然而它仍然没有摆脱脚本语言的根本问题——他们的设计者不知道他们自己在干什么 :P 采用脚本语言学编程,一个很严重的问题就是使得学习者抓不住关键。 脚本语言往往把一些系统工具性质的东西(比如正则表达式,Web 概念)加入到语法里面,导致初学者为它们浪费太多时间,却没有理解编程最关键的概念:变量,函数,递归,类型…… 不推荐 Go 语言的原因类似,虽然 Go 语言不算脚本语言,然而他的设计者显然不明白自己在干什么。所以使用 Go 语言来学编程,你不能专注于最关键,最好的语言特性。 同样的,我不觉得 Rust 适合作为入门语言。Rust 花了太大精力来夸耀它的“新特性”,而这些新特性不但不是最关键的部分,而且很多是有问题的。初学者过早的关注这些特性,不仅学不会最关键的编程思想,而且可能误入歧途。 掌握关键语言特性,忽略次要特性 为了达到我之前提到的融会贯通,一通百通的效果,初学者应该专注于语言里面最关键的特性,而不是被次要的特性分心。 举个夸张点的例子。 我发现很多编程培训班和野鸡大学的编程入门课,往往一来就教学生如何使用 printf 打印“Hello World!”,进而要他们记忆 printf 的各种“格式字符”的意义,要他们实现各种复杂格式的打印输出,甚至要求打印到文本文件里,然后再读出来…… 可是殊不知,这种输出输入操作其实根本不算是语言的一部分,而且对于掌握编程的核心概念来说,都是次要的。 有些人的 Java 课程进行了好几个星期,居然还在布置各种 printf 的作业。学生写出几百行的 printf,却不理解变量和函数是什么,甚至连算术语句和循环语句都不知道怎么用! 这就是为什么很多初学者感觉编程很难,我连 %d,%f,%.2f 的含义都记不住,还怎么学编程! 然而这些野鸡大学的“教授”头衔是如此的洗脑,以至于被他们教过的学生(比如我女朋友)到我这里请教,居然骂我净教一些没用的东西,学了连 printf 的作业都没法完成 :P 你别跟我讲 for 循环,函数什么的了…… 可不可以等几个月,等我背熟了 printf 的用法再学那些啊? 所以你就发现一旦被差劲的老师教过,这个程序员基本就毁了。就算遇到好的老师,他们也很难纠正过来。 当然这是一个夸张的例子,因为 printf 根本不算是语言特性,但这个例子从同样的角度说明了次要肤浅的语言特性带来的问题。 这里举一些次要语言特性的例子: C 语言的语句块,如果里面只有一条语句,可以不打花括号。 Go 语言的函数参数类型如果一样可以合并在一起写,比如 func foo(s string, x, y, z int, c bool) { … } Perl 把正则表达式作为语言的一种特殊语法 JavaScript 语句可以在某些时候省略句尾的分号 Haskell 和 ML 等语言的 currying 自己动手实现语言特性 在基本学会了各种语言特性,能用它们来写代码之后,下一步的进阶就是去实现它们。 只有实现了各种语言特性,你才能完全地拥有它们,成为它们的主人。否则你就只是它们的使用者,你会被语言的设计者牵着鼻子走。 有个大师说得好,完全理解一种语言最好的方法就是自己动手实现它,也就是自己写一个解释器来实现它的语义。 但我觉得这句话应该稍微修改一下:完全理解一种“语言特性”最好的方法就是自己亲自实现它。 注意我在这里把“语言”改为了“语言特性”。你并不需要实现整个语言来达到这个目的,因为我们最终使用的是语言特性。 只要你自己实现了一种语言特性,你就能理解这个特性在任何语言里的实现方式和用法。 举个例子,学习 SICP 的时候,大家都会亲自用 Scheme 实现一个面向对象系统。 用 Scheme 实现的面向对象系统,跟 Java,C++,Python 之类的语言语法相去甚远,然而它却能帮助你理解任何这些 OOP 语言里面的“面向对象”这一概念,它甚至能帮助你理解各种面向对象实现的差异。 这种效果是你直接学习 OOP 语言得不到的,因为在学习 Java,C++,Python 之类语言的时候,你只是一个用户,而用 Scheme 自己动手实现了 OO 系统之后,你成为了一个创造者。 类似的特性还包括类型推导,类型检查,惰性求值,如此等等。 我实现过几乎所有的语言特性,所以任何语言在我的面前,都是可以被任意拆卸组装的玩具,而不再是凌驾于我之上的神圣。 总结 写了这么多,重要的话重复三遍:语言特性,语言特性,语言特性,语言特性! 不管是初学者还是资深程序员,应该专注于语言特性,而不是纠结于整个的“语言品牌”。 只有这样才能达到融会贯通,拿起任何语言几乎立即就会用,并且写出高质量的代码。 点击「阅读原文」即可到 yinwang 的博客原文。 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2021-01-22 关键词: 软件 嵌入式 编程 C语言 程序

  • 无异常日志,就不能排查问题了?

    众所周知,日志是排查问题的重要手段。关于日志设计,以及怎么根据从【用户报障】环节开始到秒级定位问题这个我们下一期说(绝非套路),这一期,主要讲一下,在没有异常日志的情况下,如何定位问题。没有日志当真能排查问题,不会是标题党吧! 案例一 从最大的同性交友网站中拉取【dubbo-spring-boot-project】的代码。 然后把demo跑起来。 本场景是由真实案例改编,因为公司代码比较复杂也不方便透露,而这个demo在github上大家都能找到,既保证了原汁原味,又能让大家方便自己体验排查过程。 好了,我们先设置owner = "feichao",然后看一下控制台 一切正常 那么,当我设置成owner = "feichaozhenshuai!",再启动 看似一切都正常,那么,我们到控制台一看。 什么情况,怎么就没owner了? 这是在哪个环节出问题了?其实肥朝当初在公司遇到这个问题的时候,场景比这个复杂得多。因为公司的业务里没有owner的话,在运行时会出现一些其他异常,涉及公司业务这里就不展开了,我们言归正传,为毛我设置成feichaozhenshuai!就不行了,那我设置成肥朝大帅比电脑会不会爆炸啊??? 常见的错误做法是,把这个问题截图往群里一丢,问“你们有没有遇到过dubbo里面,owner设置不生效的问题?” 而关注了肥朝公众号的【真爱粉丝】会这么问,“dubbo里面设置owner却不生效,你们觉得我要从个角度排查问题?”。一看到这么正确的提问方式,我觉得我不回复你都不好意思。好了,回到主题,这个时候,没有一点点错误日志,但是却设置不成功,我们有哪些排查手段? 套路一 直接找set方法,看看是不是代码做了判断,防止在owner字段里面set类似肥朝真帅这种词语,避免把帅这件事走漏风声!。这么一分析似乎挺有道理对吧,那么,如何快速找到这个set方法呢?如图 public void setOwner(String owner) {    checkMultiName("owner", owner);    this.owner = owner;} 我们跟进checkMultiName代码后发现 protected static void checkProperty(String property, String value, int maxlength, Pattern pattern) {    if (StringUtils.isEmpty(value)) {        return;    }    if (value.length() > maxlength) {        throw new IllegalStateException("Invalid " + property + "=\"" + value + "\" is longer than " + maxlength);    }    if (pattern != null) {        Matcher matcher = pattern.matcher(value);        if (!matcher.matches()) {            throw new IllegalStateException("Invalid " + property + "=\"" + value + "\" contains illegal " +                    "character, only digit, letter, '-', '_' or '.' is legal.");        }    }} 从异常描述就很明显可以看出,原来owner里面是只支持-和_等这类特殊符号,!是不支持的,所以设置成不成功,和肥朝帅不帅是没关系的,和后面的!是有关系的。擦,原来是肥朝想多了,给自己加戏了!!! 当然肥朝可以告诉你,在后面的版本,修复了这个bug,日志会看得到异常了。这个时候你觉得问题就解决了? 我相信此时很多假粉就会关掉文章,或者说下次肥朝发了一些他们不喜欢看的文章(你懂的)后,他们就从此取关,但是肥朝想说,且慢动手!!! 你想嘛,万一你以后又遇到类似的问题呢?而且源码层次很深,就不是简单的搜个set方法这么简单,这次给你搜到了set方法并解决问题,简直是偶然成功。因此,我才多次强调,要持续关注肥朝,掌握更多套路。这难道是想骗你关注?我这分明是爱你啊! 那么,万一以后遇到一些吞掉异常,亦或者某些原因导致日志没打印,我们到底如何排查? 套路二 我们知道idea里面有很多好用的功能,比如肥朝之前的【看源码,我为什么推荐IDEA?】中就提到了条件断点,除此之外,还有一个被大家低估的功能,叫做异常断点。 肥朝扫了一眼,里面的单词都是小学的英语单词,因此怎么使用就不做过多解释。遇到这个问题时,我们可以这样设置异常断点。 运行起来如下: 这样,运行起来的时候,就会迅速定位到异常位置。然后一顿分析,应该很容易找出问题。 是不是有点感觉了?那我们再来一个题型练习一下。 案例二 我们先在看之前肥朝粉丝群的提问 考虑到部分粉丝不在群里,我就简单描述一下这个粉丝的问题,他代码有个异常,然后catch打异常日志,但是日志却没输出。 当然你还是不理解也没关系,我根据该粉丝的问题,给你搭建了一个最简模型的demo,模型虽然简单,但是问题是同样的,原汁原味,熟悉的配方,熟悉的味道。git地址如下:【https://gitee.com/HelloToby/springboot-run-exception】我们运行起来看一下 @Slf4jpublic class HelloSpringApplicationRunListener implements SpringApplicationRunListener {    public HelloSpringApplicationRunListener(SpringApplication application, String[] args) {    }    @Override    public void starting() {    }    @Override    public void environmentPrepared(ConfigurableEnvironment environment) {    }    @Override    public void contextPrepared(ConfigurableApplicationContext context) {        throw new RuntimeException("欢迎关注微信公众号【肥朝】");    }    @Override    public void contextLoaded(ConfigurableApplicationContext context) {    }    @Override    public void finished(ConfigurableApplicationContext context, Throwable exception) {    }} 你会发现,一运行起来进程就停止,一点日志都没。绝大部分假粉丝遇到这个情况,都是菊花一紧,一点头绪都没,又去群里问”你们有没有遇到过,Springboot一起来进程就没了,但是没有日志的问题?“。正确提问姿势肥朝已经强调过,这里不多说。那么我们用前面学到的排查套路,再来走一波 我们根据异常栈顺藤摸瓜 我们从代码中看出两个关键单词【reportFailure】、【context.close()】,经过断点我们发现,确实是会先打印日志,再关掉容器。但是为啥日志先执行,再关掉容器,日志没输出,容器就关掉了呢?因为,这个demo中,日志是全异步日志,异步日志还没执行,容器就关了,导致了日志没有输出。 该粉丝遇到的问题是类似的,他是单元测试中,代码中的异步日志还没输出,单元测试执行完进程就停止了。知道了原理解决起来也很简单,比如最简单的,跑单元测试的时候末尾先sleep一下等日志输出。 在使用Springboot中,其实经常会遇到这种,启动期间出现异常,但是日志是异步的,日志还没输出就容器停止,导致没有异常日志。知道了原理之后,要彻底解决这类问题,可以增加一个SpringApplicationRunListener。 /** * 负责应用启动时的异常输出 */@Slf4jpublic class OutstandingExceptionReporter implements SpringApplicationRunListener {    public OutstandingExceptionReporter(SpringApplication application, String[] args) {    }    @Override    public void starting() {    }    @Override    public void environmentPrepared(ConfigurableEnvironment environment) {    }    @Override    public void contextPrepared(ConfigurableApplicationContext context) {    }    @Override    public void contextLoaded(ConfigurableApplicationContext context) {    }    @Override    public void finished(ConfigurableApplicationContext context, Throwable exception) {        if (exception != null) {            log.error("application started failed",exception);            try {                Thread.sleep(100);            } catch (InterruptedException e) {                log.error("application started failed", e);            }        }    }} 再啰嗦一句,其实日志输出不了,除了这个异步日志的案例外,还有很多情况的,比如日志冲突之类的,排查套路还很多,因此,建议持续关注,每一个套路,都想和你分享! 什么是编程思想? 肥朝始终觉得,要想比别人更优秀,除了比别人更努力这个必要因素外,思维方式,也是我们必要关注的一个重点。比如在案例二中,很多同学知道了bug之后,就认为自己学到东西了,其实这个想法既正确,也不正确。 正确的地方在于,你知道了这个bug,后面遇到相同的问题,你会猜一下是不是同样的原因。 不正确的地方在于,你只知道了这个bug出现的某个场景,但是当我们遇到这个问题,应对的排查套路有哪些你并不知道。也就是说,如果这个问题过后,你排查问题的套路并没有增加,亦或者你没有能从这个问题上,发散出自己的想法,继续压榨出更多的价值,本质上,你的编程能力,其实并没有提升的。 然而,你一旦在公司时间长了,也就是我们常说的老油条,对公司的某些坑熟悉,新人遇到问题时,就容易猜对可能是某个坑。但是其实你的套路来来去去就那几个,本质上你的编程能力并没有提升,却让你产生了自己越来越牛逼,这下必须要加薪的错觉。 一个公司总是有线上报障是有问题的,但是一直不出问题也有问题的。当然很多时候,排查的机会或许轮不到你。这个时候,就会有常见的几种做法。 1.公司确实项目太简单,基本没有什么拿得出手的bug,都是一些低级的漏掉配置的bug。 2.大佬们在排查,反正不是我的问题,那我就看群吹吹水,下班美滋滋。 3.大佬们在排查,等他们有结论了,我就过去问一句是啥问题,然后暗自记下来,下次面试的时候就说是自己排查的,吹一波,美滋滋。 4.大佬们在排查,得知原因后,深入思考,大佬们为啥会想到是这个原因,他们是怎么排查的?用了哪些排查工具?排查技巧?然后暗自总结一波,并把自己代入场景,脑补一下自己来排查问题,并把这个bug压榨出更多价值!(怎么压榨出更多价值,可以查看肥朝之前的源码实战文章,每一篇都有一个环节专门讲拓展思考的) 你的思维方式,你的行动,往往就决定你成为什么样的人。 特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下: 长按订阅更多精彩▼如有收获,点个在看,诚挚感谢 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-12-08 关键词: 嵌入式 程序

  • Keil MDK如何将变量存储在指定内存地址?

    关注+星标公众号,不错过精彩内容 作者 | strongerHuang 微信公众号 | strongerHuang 变量是程序中重要的一部分,产品中很多业务代码都是变量起到了关键作用。 传感器采集的数据、通信中传输的数据、算法中的数据等这些数据都需要借助变量这个东西来实现。 我们为什么要将变量、数组(例如表或函数)存储到特定的地址? 因为有些特殊的数据可能需要指定地址,比如加密的密钥、校验和等这些数据可能需要存储在指定内存。 1变量查看工具 在描述将变量储存在指定内存地址之前,我们先来描述一下查看变量的工具(通过工具可以查看变量的值) 1.STMStudio 之前给大家分享过《STM Studio调试和诊断工具讲解》。 这里简单说一下STMStudio与本文相关的内容:变量。 STMStudio可管理四种变量: 1.由物理存储地址标识的绝对变量。 2.能够计算绝对变量的最小值、最大值、平均值和标准差等值的统计变量。 3.表达式变量是数学表达式计算的结果。表达式是绝对变量或统计变量与数学运算符(+,-,*,/…)的组合,例如:(Variable1+Variable2)*Variable3。注意,表达式变量是在统计变量之后求值的,因此不可能计算表达式的统计值。 4.包含用户可配置信息的插件变量。 (为了节约时间,使用有道翻译的句子,大家请结合原文理解) 可以看得出来,局部变量是不支持的。 2.J-Scope 这个STMStudio工具和J-Scope有类似之处,也能查看变量、波形: J-Scope也是一个不错的工具,大家可以下载安装试试: https://www.segger.com/products/debug-probes/j-link/tools/j-scope 2Keil MDK如何将变量存储在指定内存地址? 不同的编译器,实现的方法可能不同,这里主要结合MDK说下基于AC5和AC6编译,简述其中的方法。 1.针对AC5(ARMCC Compiler version 5.x) 定义一个变量cnt到指定内存地址:0x20008000 uint8_t cnt __attribute__((at(0x20008000))); 2.针对AC6(ARM Compiler 6 (又名ARMCLANG)) 定义一个变量cnt到指定内存地址:0x20008000 uint8_t cnt __attribute__((section(".ARM.__at_0x20008000"))); 这个地方进行分段: 这样指定内存地址,即可使用STMStudio进行查看指定地址变量了: 好了,先写到这里,希望对大家有帮助。 ------------ END ------------ 推荐阅读: 如何编写ARM处理器的Bootloader SEGGER的三款RTOS有什么特点? 几款优秀的支持C、C++等多种语言的在线编译器 关注 微信公众号『strongerHuang』,后台回复“1024”查看更多内容,回复“加群”按规则加入技术交流群。 长按前往图中包含的公众号关注 点击“ 阅读原文 ”查看更多分享,欢迎点分享、收藏、点赞、在看。 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-11-02 关键词: 嵌入式 程序

  • 当心!别再被大小端的问题坑了

    先说内存 柿子捡软的捏,以前做项目的时候被大小端的问题坑过,那种酸爽就像蓝天白云,晴空万里忽然暴风雨,突如其来的BUG,让原本不充裕的时间更加雪上加霜;虽然很基础,但是能力有限,也难免出现错误和纰漏,请各位大佬们在讨论中无情指正我。 先说内存 程序运行在内存中,计算机中的最小存储单位是Bit,即1和0的二进制,它可以识别的机器码就是以二进制形式存储的; 内存由多个存储单元组成,每个存储单元都有一个唯一的数字地址字节可寻址内存。每个存储位置可以包含固定数量的二进制数字。 在大多数的现代计算机上,地址的最小数据的长度为8位,称为字节(1 Byte = 8 Bit); 一般计算机中用户程序直接访问的地址是虚拟内存的地址,操作系统内核会根据用户程序访问的虚拟地址,找出页表中对于的物理地址,最终寻址到所需要的数据; 具体如下图所示; 转自wiki 然而,在MCU等裸机开发的环境中,没有MMU,则程序直接访问的是物理内存,所以无论是计算机还是MCU在程序运行中都需要内存作为载体,保存数据和运行程序。 那么,下面再来看是程序以及数据在内存中是以何种形式存储的? 字节 前面提到过,在大多数的现代计算机上,地址的最小数据的长度为8位,称为字节(1 Byte = 8 Bit);至于为什么是8位?看起来似乎有点玄学,并且很吉利的一个数字,但是老外好像没有数字迷信,这里的原因大概是因为这几点; 由于计算机内部最本质需要实现的操作是加法,减法,乘法,除法等运算都能通过加法实现,另外由于最早期设计的加法器是8位; 另外一个原因可以追溯到1956年,IBM公司最早提出字节的概念,随着IBM的壮大,字节便专门用来表示二进制数,其中也包括不少优点;易于以BCD码形式保存;用于保存文本也非常合适,另外世界上大部分语言都可以用小于256个字符(一个字节宽度)来表示,如果一个不够,那就两个,比如中文; 字节和位 字节顺序 在说大小端之前,要先提一下字节顺序(Endianness),它是描述数据以字节为一组在计算机内存中存储顺序的术语。 字节顺序可以是大端顺序(big-endian)或者小端顺序(little-endian);在对多字节数据进行存储时,一般遵循以下规则; 小端:数据的最后一个字节先存储,即 LSB; 大端:数据的第一个字节先存储,即 MSB; 数据0x01020304分别在大端机器和小端机器中的存储形式,具体如下图所示; 在大多数情况下,编译器会处理字节顺序,从而避免出现大小端不一致的问题,但是在以下情况下字节顺序就会成为一个问题。 在通讯中,例如网络编程:假设在小端机器上向文件写入整数,然后将此文件传输到大端机器上。如果没有做大小端转换,那么大端机器就会以相反的顺序读取文件。 TCP/IP协议中,默认使用的是大端顺序,它与具体的CPU类型、操作系统等无关; 那么如何在程序中快速的区分大小端呢? 大端和小端的区分 下面介绍几种通过C语言实现大小端判断的方法; 第一种通过指针的内存对齐来实现; 函数的形式; unsigned char check_endian( void ){    int test_var = 1;    unsigned char *test_endian = (unsigned char*)&test_var;    return (test_endian[0] == 0);} 宏定义的形式; static uint32_t endianness = 0xdeadbeef; enum endianness { BIG, LITTLE };#define ENDIANNESS ( *(const char *)&endianness == 0xef ? LITTLE \                   : *(const char *)&endianness == 0xde ? BIG \                   : assert(0)) 更加简洁; #define IS_BIG_ENDIAN (!*(unsigned char *)&(uint16_t){1}) 第二种通过结构体和联合体的内存对齐来实现; #ifndef ORDER32_H#define ORDER32_H#include #include #if CHAR_BIT != 8#error "unsupported char size"#endifenum{    O32_LITTLE_ENDIAN = 0x03020100ul,    O32_BIG_ENDIAN = 0x00010203ul,    O32_PDP_ENDIAN = 0x01000302ul,      /* DEC PDP-11 (aka ENDIAN_LITTLE_WORD) */    O32_HONEYWELL_ENDIAN = 0x02030001ul /* Honeywell 316 (aka ENDIAN_BIG_WORD) */};static const union { unsigned char bytes[4]; uint32_t value; } o32_host_order =    { { 0, 1, 2, 3 } };#define O32_HOST_ORDER (o32_host_order.value)#endif 当然具体的方法还有很多,本文就先讲到这里。 —— The End —— 长按识别二维码关注获取更多内容 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-11-02 关键词: 嵌入式 程序

  • 程序员必备的基本算法:递归详解

    前言 递归是一种非常重要的算法思想,无论你是前端开发,还是后端开发,都需要掌握它。在日常工作中,统计文件夹大小,解析xml文件等等,都需要用到递归算法。它太基础太重要了,这也是为什么面试的时候,面试官经常让我们手写递归算法。本文呢,将跟大家一起学习递归算法~ 什么是递归? 递归的特点 递归与栈的关系 递归应用场景 递归解题思路 leetcode案例分析 递归可能存在的问题以及解决方案 什么是递归? 递归,在计算机科学中是指一种通过重复将问题分解为同类的子问题而解决问题的方法。简单来说,递归表现为函数调用函数本身。在知乎看到一个比喻递归的例子,个人觉得非常形象,大家看一下: 递归最恰当的比喻,就是查词典。我们使用的词典,本身就是递归,为了解释一个词,需要使用更多的词。当你查一个词,发现这个词的解释中某个词仍然不懂,于是你开始查这第二个词,可惜,第二个词里仍然有不懂的词,于是查第三个词,这样查下去,直到有一个词的解释是你完全能看懂的,那么递归走到了尽头,然后你开始后退,逐个明白之前查过的每一个词,最终,你明白了最开始那个词的意思。 来试试水,看一个递归的代码例子吧,如下: public int sum(int n) {    if (n 

    时间:2020-11-02 关键词: 递归算法 程序

  • 1分钟搞定Scrapy分布式爬虫、队列和布隆过滤器

    使用Scrapy开发一个分布式爬虫?你知道最快的方法是什么吗?一分钟真的能 开发好或者修改出 一个分布式爬虫吗? 话不多说,先让我们看看怎么实践,再详细聊聊细节。 快速上手 Step 0: 首先安装 Scrapy-Distributed : pip install scrapy-distributed 如果你没有所需要的运行条件,你可以启动两个 Docker 镜像进行测试 (RabbitMQ 和 RedisBloom): # pull and run a RabbitMQ container. docker run -d --name rabbitmq -p 0.0.0.0:15672:15672 -p 0.0.0.0:5672:5672 rabbitmq:3 # pull and run a RedisBloom container. docker run -d --name redis-redisbloom -p 0.0.0.0:6379:6379 redislabs/rebloom:latest Step 1 (非必须): 如果你有一个现成的爬虫,可以跳过这个 Step,直接到 Step 2。 创建一个爬虫工程,我这里以一个 sitemap 爬虫为例: scrapy startproject simple_example 然后修改 spiders 文件夹下的爬虫程序文件: from scrapy_distributed.spiders.sitemap import SitemapSpiderfrom scrapy_distributed.queues.amqp import QueueConfigfrom scrapy_distributed.dupefilters.redis_bloom import RedisBloomConfigclass MySpider(SitemapSpider):    name = "example"    sitemap_urls = ["http://www.people.com.cn/robots.txt"]    queue_conf: QueueConfig = QueueConfig(        name="example", durable=True, arguments={"x-queue-mode": "lazy", "x-max-priority": 255}    )    redis_bloom_conf: RedisBloomConfig = RedisBloomConfig(key="example:dupefilter")    def parse(self, response):        self.logger.info(f"parse response, url: {response.url}") Step 2: 只需要修改配置文件 settings.py 下的SCHEDULER, DUPEFILTER_CLASS 并且添加 RabbitMQ和 Redis 的相关配置,你就可以马上获得一个分布式爬虫,Scrapy-Distributed 会帮你初始化一个默认配置的 RabbitMQ 队列和一个默认配置的 RedisBloom 布隆过滤器。 # 同时集成 RabbitMQ 和 RedisBloom 的 Scheduler# 如果仅使用 RabbitMQ 的 Scheduler,这里可以填 scrapy_distributed.schedulers.amqp.RabbitSchedulerSCHEDULER = "scrapy_distributed.schedulers.DistributedScheduler"SCHEDULER_QUEUE_CLASS = "scrapy_distributed.queues.amqp.RabbitQueue"RABBITMQ_CONNECTION_PARAMETERS = "amqp://guest:guest@localhost:5672/example/?heartbeat=0"DUPEFILTER_CLASS = "scrapy_distributed.dupefilters.redis_bloom.RedisBloomDupeFilter"BLOOM_DUPEFILTER_REDIS_URL = "redis://:@localhost:6379/0"BLOOM_DUPEFILTER_REDIS_HOST = "localhost"BLOOM_DUPEFILTER_REDIS_PORT = 6379# Redis Bloom 的客户端配置,复制即可REDIS_BLOOM_PARAMS = {    "redis_cls": "redisbloom.client.Client"}# 布隆过滤器误判率配置,不写配置的情况下默认为 0.001BLOOM_DUPEFILTER_ERROR_RATE = 0.001# 布隆过滤器容量配置,不写配置的情况下默认为 100_0000BLOOM_DUPEFILTER_CAPACITY = 100_0000 你也可以给你的 Spider 类,增加两个类属性,来初始化你的 RabbitMQ 队列或 RedisBloom 布隆过滤器: class MySpider(SitemapSpider):    ......    # 通过 arguments 参数,可以配置更多参数,这里示例配置了 lazy 模式和优先级最大值    queue_conf: QueueConfig = QueueConfig(        name="example", durable=True, arguments={"x-queue-mode": "lazy", "x-max-priority": 255}    )    # 通过 key,error_rate,capacity 分别配置布隆过滤器的redis key,误判率,和容量    redis_bloom_conf: RedisBloomConfig = RedisBloomConfig(key="example:dupefilter", error_rate=0.001, capacity=100_0000)    ...... Step 3: scrapy crawl example 检查一下你的 RabbitMQ 队列 和 RedisBloom 过滤器,是不是已经正常运行了? 可以看到,Scrapy-Distributed 的加持下,我们只需要修改配置文件,就可以将普通爬虫修改成支持 RabbitMQ 队列 和 RedisBloom 布隆过滤器的分布式爬虫。在拥有 RabbitMQ 和 RedisBloom 环境的情况下,修改配置的时间也就一分钟。 关于Scrapy-Distributed 目前 Scrapy-Distributed 主要参考了Scrapy-Redis 和 scrapy-rabbitmq 这两个库。 如果你有过 Scrapy 的相关经验,可能会知道 Scrapy-Redis 这个库,可以很快速的做分布式爬虫,如果你尝试过使用 RabbitMQ 作为爬虫的任务队列,你可能还见到过 scrapy-rabbitmq 这个项目。诚然 Scrapy-Redis 已经很方便了,scrapy-rabbitmq 也能实现 RabbitMQ 作为任务队列,但是他们存在一些缺陷,我这里简单提出几个问题。 Scrapy-Redis 使用 Redis 的 set 去重,链接数量越大占用的内存就越大,不适合任务数量大的分布式爬虫。 Scrapy-Redis 使用 Redis 的 list 作为队列,很多场景会有任务积压,会导致内存资源消耗过快,比如我们爬取网站 sitemap 时,链接入队的速度远远大于出队。 scrapy-rabbitmq 等 RabbitMQ 的 Scrapy 组件,在创建队列方面,没有提供 RabbitMQ 支持的各种参数,无法控制队列的持久化等参数。 scrapy-rabbitmq 等 rabbitmq 框架的 Scheduler 暂未支持分布式的 dupefilter ,需要使用者自行开发或接入相关组件。 Scrapy-Redis 和 scrapy-rabbitmq 等框架都是侵入式的,如果需要用这些框架开发分布式的爬虫,需要我们修改自己的爬虫代码,通过继承框架的 Spider 类,才能实现分布式功能。 于是,Scrapy-Distributed 框架就在这个时候诞生了,在非侵入式设计下,你只需要通过修改 settings.py 下的配置,框架就可以根据默认配置将你的爬虫分布式化。 为了解决Scrapy-Redis 和 scrapy-rabbitmq 存在的一些痛点,Scrapy-Distributed 做了下面几件事: 采用了 RedisBloom 的布隆过滤器,内存占用更少。 支持了 RabbitMQ 队列声明的所有参数配置,可以让 RabbitMQ 队列支持 lazy-mode 模式,将减少内存占用。 RabbitMQ 的队列声明更加灵活,不同爬虫可以使用相同队列配置,也可以使用不同的队列配置。 Scheduler 的设计上支持多个组件的搭配组合,可以单独使用 RedisBloom 的DupeFilter,也可以单独使用 RabbitMQ 的 Scheduler 模块。 实现了 Scrapy 分布式化的非侵入式设计,只需要修改配置,就可以将普通爬虫分布式化。 作者:许臾insutanto来源:https://insutanto.net/posts/scrapy/ 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-11-02 关键词: 应用框架 程序

  • ISP自动下载程序电路

    本文转自瑞生网,非常感谢瑞生。   STM32支持仿真器和串口下载程序。将要介绍的内容,属于串口下载,即我们通常说的ISP下载。 手动ISP下载程序,我们已经知道了,控制BOOT0引脚实现。STM32上电,会自动检测BOOT0引脚是什么电平,如果是高电平,等待用户下载程序;如果是低电平,运行用户之前下载到单片机的程序。所以我们需要把BOOT0引脚引出,然后控制其接地或接VCC来下载程序或者运行程序。在调试过程中,我们需要不断的控制BOOT0,非常麻烦。那么,自动ISP就该出场了。 自动ISP,把BOOT0与地直接连接,那么每次上电就会运行程序,而且只要点击电脑上的“下载”按钮,就开始下载程序,下载完程序,就开始执行。实现此目的,需要借助串口握手信号DTR和RTS。 下面是深圳鹏远电通科技有限公司研发的免费ISP下载软件,请看红色的框里面的部分。   DTR连接RESET(复位引脚),控制复位,RTS连接BOOT0,用来控制程序运行或者等待下载。 下面说明如何用USB转TTL芯片实现STM32自动ISP。CH340芯片如下图所示:   CH340芯片,DTR和RTS引脚在一般情况下是高电平,低电平有效。因为STM32的RESET引脚,也是一般情况下是高电平,低电平复位,所以DTR可以与RESET直接连接。但是BOOT0是高电平下载程序,低电平运行程序,正好与RTS相反,所以我们需要把它反相,加一个NPN三极管即可。电路如下图所示: ISP软件的选择: 看了上图的选择,有些人一定会产生疑虑。按道理应该是低电平复位,然后低电平进入BOOTLoader呀。但是,有一点需要注意,它这里讲的高低电平,是针对电脑原始的9针串口的,也就是“232电平”,我们用的USB转TTL芯片是“TTL电平”,正好相反。 下载过程和结果如下图所示:   注意:CH340在刚上电,稳定需要几秒钟时间,在此期间,DTR引脚会有两次或者三次的变低情况,这样会引起单片机上电后复位两三次,稳定后不会影响程序运行。如果不想让单片机上电复位好几次,上电的时候把DTR与RESET断开即可。 文章来源http://www.rationmcu.com/elecjc/947.html 关注 微信公众号『玩转嵌入式』,后台回复“128”获取干货资料汇总,回复“256”加入技术交流群。 精彩技术文章推荐 01 |  插入排序:最直观的排序算法 02 |  怎样才能学好编程?懂语法、多写、锻炼思维 03 | 单片机编程如何查看版本之间代码的不同:代码比较工具 04 |  你在编程时,都是怎样控制程序版本的呢? 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-10-27 关键词: 嵌入式 程序

  • 结合Keil描述如何编写ARM处理器的Bootloader

    关注+星标公众号,不错过精彩内容 作者 | strongerHuang 微信公众号 | strongerHuang 之前从应用的角度给大家分享过Bootloader相关的文章,今天从底层原理来给大家描述ARM处理器如何编写Bootloader。 1关于Bootloader Bootloader顾名思义就是引导加载程序,是在操作系统或应用程序运行之前的一段程序,是在系统上电后执行的一段程序代码。 BootLoader是严重地依赖于硬件而实现的,特别是在嵌入式平台。因此,在嵌入式平台里建立一个通用的BootLoader几乎是不可能的。尽管如此,我们仍然可以对bootloader归纳出一些通用的概念来,以指导用户特定的BootLoader设计与实现。 ---来源百度百科 Bootloader在手机、电脑、众多嵌入式系统中都存在,它的作用有很多,比如:初始化底层应用驱动、加载应用程序、更新应用程序等。 不同的设备,Bootloader可能差异很大,通常来说Bootloader比较依赖底层硬件和实际项目需求。 2如何编写bootloader bootloader是一段引导加载程序代码,它更新用户的应用程序代码,可以使用很多硬件下载通道(例如USB、网络端口)获得新代码。 在执行引导ROM之后,将执行bootloader程序,并在需要时进行更新,然后执行最终用户应用程序。 引导加载程序和用户应用程序应作为两个独立的Project或Object进行编写和编译,从而产生两个独立且可执行的(bin/hex)文件。 引导加载程序的主要任务是在必要时对用户应用程序进行重新编程/替换,并跳转至用户应用程序以执行该程序,应用程序不一定需要知道引导加载程序的存在。 引导加载程序通常位于芯片闪存基址,下面通过一张图来描述内存和Flash代码映射关系: 有很多方法可以引导bootloader进入编程模式,以将用户应用程序重新编程到Flash中,或者直接跳转到现有的用户应用程序来执行。最简单的方法是检查GPIO引脚以确定是否应进入编程模式。 大多数芯片供应商为用户提供了一种方便的方式,例如 ISP 和 IAP 接口,bootloader将使用它们来更新闪存内容。 当Flash内容已更新或已经是最新时,引导加载程序将跳转到用户应用程序。在执行用户应用程序之前,这需要许多步骤: 1.确保CPU处于特权模式。 2.禁用NVIC中所有启用的中断。 3.禁用所有可能产生中断请求的使能外设,并清除这些外设中的所有未使用中断标志。 4.清除NVIC中所有未使用的中断请求。 5.禁用SysTick并清除其异常挂起位。 6.如果引导加载程序使用了单个故障处理程序,请禁用它们。 7.如果发现内核当前与PSP一起运行,则激活MSP(由于编译器可能仍在使用堆栈,因此在此之前需要将PSP复制到MSP)。 8.将用户应用程序的向量表地址加载到SCB-> VTOR寄存器中。确保地址符合对齐要求。 9.最后一部分是将MSP设置为用户应用程序向量表中找到的值,然后将用户应用程序的重置向量值加载到PC中,也就是跳转功能。 比如通过调用下面的示例BootJump()这样的函数来完成此操作: static void BootJump(uint32_t *Address){ //1.确保CPU处于特权模式。  if( CONTROL_nPRIV_Msk & __get_CONTROL()) { /* not in privileged mode */    EnablePrivilegedMode() ; } //2.禁用NVIC中所有启用的中断。 Disable_All_Peripherals(); //3.禁用所有可能产生中断请求的使能外设,并清除这些外设中的所有未使用中断标志。 NVIC->ICER[ 0 ] = 0xFFFFFFFF; NVIC->ICER[ 1 ] = 0xFFFFFFFF; NVIC->ICER[ 2 ] = 0xFFFFFFFF; NVIC->ICER[ 3 ] = 0xFFFFFFFF; NVIC->ICER[ 4 ] = 0xFFFFFFFF; NVIC->ICER[ 5 ] = 0xFFFFFFFF; NVIC->ICER[ 6 ] = 0xFFFFFFFF; NVIC->ICER[ 7 ] = 0xFFFFFFFF; //4.清除NVIC中所有未使用的中断请求。 NVIC->ICPR[ 0 ] = 0xFFFFFFFF; NVIC->ICPR[ 1 ] = 0xFFFFFFFF; NVIC->ICPR[ 2 ] = 0xFFFFFFFF; NVIC->ICPR[ 3 ] = 0xFFFFFFFF; NVIC->ICPR[ 4 ] = 0xFFFFFFFF; NVIC->ICPR[ 5 ] = 0xFFFFFFFF; NVIC->ICPR[ 6 ] = 0xFFFFFFFF; NVIC->ICPR[ 7 ] = 0xFFFFFFFF; //5.禁用SysTick并清除其异常挂起位。 SysTick->CTRL = 0; SCB->ICSR |= SCB_ICSR_PENDSTCLR_Msk; //6.如果引导加载程序使用了单个故障处理程序,请禁用它们。 SCB->SHCSR &= ~( SCB_SHCSR_USGFAULTENA_Msk | \ SCB_SHCSR_BUSFAULTENA_Msk | \ SCB_SHCSR_MEMFAULTENA_Msk ) ; //7.如果发现内核当前与PSP一起运行,则激活MSP  if( CONTROL_SPSEL_Msk & __get_CONTROL()) { /* MSP is not active */    __set_MSP( __get_PSP()) ;    __set_CONTROL( __get_CONTROL() & ~CONTROL_SPSEL_Msk); } //8.将用户应用程序的向量表地址加载到SCB-> VTOR寄存器中。 SCB->VTOR = ( uint32_t )Address ; //9.跳转  BootJumpASM( Address[ 0 ], Address[ 1 ]);} 再次说明bootloader与底层硬件和实际需求有关,以上代码仅供参考,主要是提供思路,方便大家理解。 如果还不能理解,建议结合bootloader实际项目进行理解,比如之前给大家分享过的:STM32 + IAP + Ymodem完美结合 ------------ END ------------ 推荐阅读: 程序猿如何选择开源协议? 线程、进程、多线程、多进程 和 多任务  几款优秀的支持C、C++等多种语言的在线编译器 关注 微信公众号『strongerHuang』,后台回复“1024”查看更多内容,回复“加群”按规则加入技术交流群。 长按前往图中包含的公众号关注 点击“ 阅读原文 ”查看更多分享,欢迎点分享、收藏、点赞、在看。 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-10-27 关键词: 嵌入式 代码 程序

  • 如何写出让CPU跑得更快的代码?

    前言 代码都是由 CPU 跑起来的,我们代码写的好与坏就决定了 CPU 的执行效率,特别是在编写计算密集型的程序,更要注重 CPU 的执行效率,否则将会大大影响系统性能。 CPU 内部嵌入了 CPU Cache(高速缓存),它的存储容量很小,但是离 CPU 核心很近,所以缓存的读写速度是极快的,那么如果 CPU 运算时,直接从 CPU Cache 读取数据,而不是从内存的话,运算速度就会很快。 但是,大多数人不知道 CPU Cache 的运行机制,以至于不知道如何才能够写出能够配合 CPU Cache 工作机制的代码,一旦你掌握了它,你写代码的时候,就有新的优化思路了。 那么,接下来我们就来看看,CPU Cache 到底是什么样的,是如何工作的呢,又该写出让 CPU 执行更快的代码呢? 正文 CPU Cache 有多快? 你可能会好奇为什么有了内存,还需要 CPU Cache?根据摩尔定律,CPU 的访问速度每 18 个月就会翻倍,相当于每年增长 60% 左右,内存的速度当然也会不断增长,但是增长的速度远小于 CPU,平均每年只增长 7% 左右。于是,CPU 与内存的访问性能的差距不断拉大。 到现在,一次内存访问所需时间是 200~300 多个时钟周期,这意味着 CPU 和内存的访问速度已经相差 200~300 多倍了。 为了弥补 CPU 与内存两者之间的性能差异,就在 CPU 内部引入了  CPU Cache,也称高速缓存。 CPU Cache 通常分为大小不等的三级缓存,分别是 L1 Cache、L2 Cache 和 L3 Cache。 由于 CPU Cache 所使用的材料是 SRAM,价格比内存使用的 DRAM 高出很多,在当今每生产 1 MB 大小的 CPU Cache 需要 7 美金的成本,而内存只需要 0.015 美金的成本,成本方面相差了 466 倍,所以 CPU Cache 不像内存那样动辄以 GB 计算,它的大小是以 KB 或 MB 来计算的。 在 Linux 系统中,我们可以使用下图的方式来查看各级 CPU Cache 的大小,比如我这手上这台服务器,离 CPU 核心最近的 L1 Cache 是 32KB,其次是 L2 Cache 是 256KB,最大的 L3 Cache 则是 3MB。 其中,L1 Cache 通常会分为「数据缓存」和「指令缓存」,这意味着数据和指令在 L1 Cache 这一层是分开缓存的,上图中的 index0 也就是数据缓存,而 index1 则是指令缓存,它两的大小通常是一样的。 另外,你也会注意到,L3 Cache 比 L1 Cache 和 L2 Cache 大很多,这是因为 L1 Cache 和 L2 Cache 都是每个 CPU 核心独有的,而 L3 Cache 是多个 CPU 核心共享的。 程序执行时,会先将内存中的数据加载到共享的 L3 Cache 中,再加载到每个核心独有的 L2 Cache,最后进入到最快的 L1 Cache,之后才会被 CPU 读取。它们之间的层级关系,如下图: 越靠近 CPU 核心的缓存其访问速度越快,CPU 访问 L1 Cache 只需要 2~4 个时钟周期,访问 L2 Cache 大约 10~20 个时钟周期,访问 L3 Cache 大约 20~60 个时钟周期,而访问内存速度大概在 200~300 个 时钟周期之间。如下表格: 所以,CPU 从 L1 Cache 读取数据的速度,相比从内存读取的速度,会快 100 多倍。 CPU Cache 的数据结构和读取过程是什么样的? CPU Cache 的数据是从内存中读取过来的,它是以一小块一小块读取数据的,而不是按照单个数组元素来读取数据的,在 CPU Cache 中的,这样一小块一小块的数据,称为 Cache Line(缓存块)。 你可以在你的 Linux 系统,用下面这种方式来查看 CPU 的 Cache Line,你可以看我服务器的 L1 Cache Line 大小是 64 字节,也就意味着 L1 Cache 一次载入数据的大小是 64 字节。 比如,有一个 int array[100] 的数组,当载入 array[0] 时,由于这个数组元素的大小在内存只占 4 字节,不足 64 字节,CPU 就会顺序加载数组元素到 array[15],意味着 array[0]~array[15] 数组元素都会被缓存在 CPU Cache 中了,因此当下次访问这些数组元素时,会直接从 CPU Cache 读取,而不用再从内存中读取,大大提高了 CPU 读取数据的性能。 事实上,CPU 读取数据的时候,无论数据是否存放到 Cache 中,CPU 都是先访问 Cache,只有当 Cache 中找不到数据时,才会去访问内存,并把内存中的数据读入到 Cache 中,CPU 再从 CPU Cache 读取数据。 这样的访问机制,跟我们使用「内存作为硬盘的缓存」的逻辑是一样的,如果内存有缓存的数据,则直接返回,否则要访问龟速一般的硬盘。 那 CPU 怎么知道要访问的内存数据,是否在 Cache 里?如果在的话,如何找到 Cache 对应的数据呢?我们从最简单、基础的直接映射 Cache(Direct Mapped Cache) 说起,来看看整个 CPU Cache 的数据结构和访问逻辑。 前面,我们提到 CPU 访问内存数据时,是一小块一小块数据读取的,具体这一小块数据的大小,取决于 coherency_line_size 的值,一般 64 字节。在内存中,这一块的数据我们称为内存块(Bock),读取的时候我们要拿到数据所在内存块的地址。 对于直接映射 Cache 采用的策略,就是把内存块的地址始终「映射」在一个 CPU Line(缓存块) 的地址,至于映射关系实现方式,则是使用「取模运算」,取模运算的结果就是内存块地址对应的 CPU Line(缓存块) 的地址。 举个例子,内存共被划分为 32 个内存块,CPU Cache 共有 8 个 CPU Line,假设 CPU 想要访问第 15 号内存块,如果 15 号内存块中的数据已经缓存在 CPU Line 中的话,则是一定映射在 7 号 CPU Line 中,因为 15 % 8 的值是 7。 机智的你肯定发现了,使用取模方式映射的话,就会出现多个内存块对应同一个 CPU Line,比如上面的例子,除了 15 号内存块是映射在 7 号 CPU Line 中,还有 7 号、23 号、31 号内存块都是映射到 7 号 CPU Line 中。 因此,为了区别不同的内存块,在对应的 CPU Line 中我们还会存储一个组标记(Tag)。这个组标记会记录当前 CPU Line 中存储的数据对应的内存块,我们可以用这个组标记来区分不同的内存块。 除了组标记信息外,CPU Line 还有两个信息: 一个是,从内存加载过来的实际存放数据(Data)。 另一个是,有效位(Valid bit),它是用来标记对应的 CPU Line 中的数据是否是有效的,如果有效位是 0,无论 CPU Line 中是否有数据,CPU 都会直接访问内存,重新加载数据。 CPU 在从 CPU Cache 读取数据的时候,并不是读取 CPU Line 中的整个数据块,而是读取 CPU 所需要的一个数据片段,这样的数据统称为一个字(Word)。那怎么在对应的 CPU Line 中数据块中找到所需的字呢?答案是,需要一个偏移量(Offset)。 因此,一个内存的访问地址,包括组标记、CPU Line 索引、偏移量这三种信息,于是 CPU 就能通过这些信息,在 CPU Cache 中找到缓存的数据。而对于 CPU Cache 里的数据结构,则是由索引 + 有效位 + 组标记 + 数据块组成。 如果内存中的数据已经在 CPU Cahe 中了,那 CPU 访问一个内存地址的时候,会经历这 4 个步骤: 根据内存地址中索引信息,计算在 CPU Cahe 中的索引,也就是找出对应的 CPU Line 的地址; 找到对应 CPU Line 后,判断 CPU Line 中的有效位,确认 CPU Line 中数据是否是有效的,如果是无效的,CPU 就会直接访问内存,并重新加载数据,如果数据有效,则往下执行; 对比内存地址中组标记和 CPU Line 中的组标记,确认 CPU Line 中的数据是我们要访问的内存数据,如果不是的话,CPU 就会直接访问内存,并重新加载数据,如果是的话,则往下执行; 根据内存地址中偏移量信息,从 CPU Line 的数据块中,读取对应的字。 到这里,相信你对直接映射 Cache 有了一定认识,但其实除了直接映射 Cache 之外,还有其他通过内存地址找到 CPU Cache 中的数据的策略,比如全相连 Cache (Fully Associative Cache)、组相连 Cache (Set Associative Cache)等,这几种策策略的数据结构都比较相似,我们理解流直接映射 Cache 的工作方式,其他的策略如果你有兴趣去看,相信很快就能理解的了。 如何写出让 CPU 跑得更快的代码? 我们知道 CPU 访问内存的速度,比访问 CPU Cache 的速度慢了 100 多倍,所以如果 CPU 所要操作的数据在 CPU Cache 中的话,这样将会带来很大的性能提升。访问的数据在 CPU Cache 中的话,意味着缓存命中,缓存命中率越高的话,代码的性能就会越好,CPU 也就跑的越快。 于是,「如何写出让 CPU 跑得更快的代码?」这个问题,可以改成「如何写出 CPU 缓存命中率高的代码?」。 在前面我也提到, L1 Cache 通常分为「数据缓存」和「指令缓存」,这是因为 CPU 会别处理数据和指令,比如 1+1=2 这个运算,+ 就是指令,会被放在「指令缓存」中,而输入数字 1 则会被放在「数据缓存」里。 因此,我们要分开来看「数据缓存」和「指令缓存」的缓存命中率。 如何提升数据缓存的命中率? 假设要遍历二维数组,有以下两种形式,虽然代码执行结果是一样,但你觉得哪种形式效率最高呢?为什么高呢? 经过测试,形式一 array[i][j]  执行时间比形式二 array[j][i] 快好几倍。 之所以有这么大的差距,是因为二维数组 array 所占用的内存是连续的,比如长度 N 的指是 2 的话,那么内存中的数组元素的布局顺序是这样的: 形式一用 array[i][j]  访问数组元素的顺序,正是和内存中数组元素存放的顺序一致。当 CPU 访问 array[0][0] 时,由于该数据不在 Cache 中,于是会「顺序」把跟随其后的 3 个元素从内存中加载到 CPU Cache,这样当 CPU 访问后面的 3 个数组元素时,就能在 CPU Cache 中成功地找到数据,这意味着缓存命中率很高,缓存命中的数据不需要访问内存,这便大大提高了代码的性能。 而如果用形式二的 array[j][i] 来访问,则访问的顺序就是: 你可以看到,访问的方式跳跃式的,而不是顺序的,那么如果 N 的数值很大,那么操作 array[j][i] 时,是没办法把 array[j+1][i] 也读入到 CPU Cache 中的,既然 array[j+1][i] 没有读取到 CPU Cache,那么就需要从内存读取该数据元素了。很明显,这种不连续性、跳跃式访问数据元素的方式,可能不能充分利用到了 CPU Cache 的特性,从而代码的性能不高。 那访问 array[0][0] 元素时,CPU 具体会一次从内存中加载多少元素到 CPU Cache 呢?这个问题,在前面我们也提到过,这跟 CPU Cache Line 有关,它表示 CPU Cache 一次性能加载数据的大小,可以在 Linux 里通过 coherency_line_size 配置查看 它的大小,通常是 64 个字节。 也就是说,当 CPU 访问内存数据时,如果数据不在 CPU Cache 中,则会一次性会连续加载 64 字节大小的数据到 CPU Cache,那么当访问 array[0][0] 时,由于该元素不足 64 字节,于是就会往后顺序读取 array[0][0]~array[0][15] 到 CPU Cache 中。顺序访问的 array[i][j] 因为利用了这一特点,所以就会比跳跃式访问的 array[j][i] 要快。 因此,遇到这种遍历数组的情况时,按照内存布局顺序访问,将可以有效的利用 CPU Cache 带来的好处,这样我们代码的性能就会得到很大的提升, 如何提升指令缓存的命中率? 提升数据的缓存命中率的方式,是按照内存布局顺序访问,那针对指令的缓存该如何提升呢? 我们以一个例子来看看,有一个元素为 0 到 100 之间随机数字组成的一维数组: 接下来,对这个数组做两个操作: 第一个操作,循环遍历数组,把小于 50 的数组元素置为 0; 第二个操作,将数组排序; 那么问题来了,你觉得先遍历再排序速度快,还是先排序再遍历速度快呢? 在回答这个问题之前,我们先了解 CPU 的分支预测器。对于 if 条件语句,意味着此时至少可以选择跳转到两段不同的指令执行,也就是 if 还是 else 中的指令。那么,如果分支预测可以预测到接下来要执行 if 里的指令,还是 else 指令的话,就可以「提前」把这些指令放在指令缓存中,这样 CPU 可以直接从 Cache 读取到指令,于是执行速度就会很快。 当数组中的元素是随机的,分支预测就无法有效工作,而当数组元素都是顺序的,分支预测器会动态地根据历史命中数据对未来进行预测,这样命中率就会很高。 因此,先排序再遍历速度会更快,这是因为排序之后,数字是从小到大的,那么前几次循环命中 if < 50 的次数会比较多,于是分支预测就会缓存 if 里的 array[i] = 0 指令到 Cache 中,后续 CPU 执行该指令就只需要从 Cache 读取就好了。 如果你肯定代码中的 if 中的表达式判断为 true 的概率比较高,我们可以使用显示分支预测工具,比如在 C/C++ 语言中编译器提供了 likely 和 unlikely 这两种宏,如果 if 条件为 ture 的概率大,则可以用 likely 宏把 if 里的表达式包裹起来,反之用 unlikely 宏。 实际上,CPU 自身的动态分支预测已经是比较准的了,所以只有当非常确信 CPU 预测的不准,且能够知道实际的概率情况时,才建议使用这两种宏。 如果提升多核 CPU 的缓存命中率? 在单核 CPU,虽然只能执行一个进程,但是操作系统给每个进程分配了一个时间片,时间片用完了,就调度下一个进程,于是各个进程就按时间片交替地占用 CPU,从宏观上看起来各个进程同时在执行。 而现代 CPU 都是多核心的,进程可能在不同 CPU 核心来回切换执行,这对 CPU Cache 不是有利的,虽然 L3 Cache 是多核心之间共享的,但是 L1 和 L2 Cache 都是每个核心独有的,如果一个进程在不同核心来回切换,各个核心的缓存命中率就会受到影响,相反如果进程都在同一个核心上执行,那么其数据的 L1 和 L2 Cache 的缓存命中率可以得到有效提高,缓存命中率高就意味着 CPU 可以减少访问 内存的频率。 当有多个同时执行「计算密集型」的线程,为了防止因为切换到不同的核心,而导致缓存命中率下降的问题,我们可以把线程绑定在某一个 CPU 核心上,这样性能可以得到非常可观的提升。 在 Linux 上提供了 sched_setaffinity 方法,来实现将线程绑定到某个 CPU 核心这一功能。 总结 由于随着计算机技术的发展,CPU 与 内存的访问速度相差越来越多,如今差距已经高达好几百倍了,所以 CPU 内部嵌入了 CPU Cache 组件,作为内存与 CPU 之间的缓存层,CPU Cache 由于离 CPU 核心很近,所以访问速度也是非常快的,但由于所需材料成本比较高,它不像内存动辄几个 GB 大小,而是仅有几十 KB 到 MB 大小。 当 CPU 访问数据的时候,先是访问 CPU Cache,如果缓存命中的话,则直接返回数据,就不用每次都从内存读取速度了。因此,缓存命中率越高,代码的性能越好。 但需要注意的是,当 CPU 访问数据时,如果 CPU Cache 没有缓存该数据,则会从内存读取数据,但是并不是只读一个数据,而是一次性读取一块一块的数据存放到 CPU Cache 中,之后才会被 CPU 读取。 内存地址映射到 CPU Cache 地址里的策略有很多种,其中比较简单是直接映射 Cache,它巧妙的把内存地址拆分成「索引 + 组标记 + 偏移量」的方式,使得我们可以将很大的内存地址,映射到很小的 CPU Cache 地址里。 要想写出让 CPU 跑得更快的代码,就需要写出缓存命中率高的代码,CPU L1 Cache 分为数据缓存和指令缓存,因而需要分别提高它们的缓存命中率: 对于数据缓存,我们在遍历数据的时候,应该按照内存布局的顺序操作,这是因为 CPU Cache 是根据 CPU Cache Line 批量操作数据的,所以顺序地操作连续内存数据时,性能能得到有效的提升; 对于指令缓存,有规律的条件分支语句能够让 CPU 的分支预测器发挥作用,进一步提高执行的效率; 另外,对于多核 CPU 系统,线程可能在不同 CPU 核心来回切换,这样各个核心的缓存命中率就会受到影响,于是要想提高进程的缓存命中率,可以考虑把线程绑定 CPU 到某一个 CPU 核心。 絮叨 分享个喜事,小林平日里忙着输出文章,今天收到一份特别的快递,是 CSDN 寄来的奖状。 骄傲的说,你们关注的是 CSDN 首届技术原创第一名的博主,以后简历又可以吹牛逼了 没有啦,其实主要还是谢谢你们不离不弃的支持。 哈喽,我是小林,就爱图解计算机基础,如果觉得文章对你有帮助,欢迎分享给你的朋友,也给小林点个「在看」,这对小林非常重要,谢谢你们,给各位小姐姐小哥哥们抱拳了,我们下次见! 推荐阅读 这个星期不知不觉输出了 3 篇文章了,前面的 2 篇还没看过的同学,赶紧去看看呀! 天啦噜!知道硬盘很慢,但没想到比 CPU Cache 慢 10000000 倍 CPU 执行程序的秘密,藏在了这 15 张图里 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-10-20 关键词: CPU 程序

  • 代码能看懂,但是为什么不会写?

    程序代码是主观性很强的东西,同样的一个功能,每一个程序员所写出来的代码都不一样,所以那句名言“一千个读者就有一千个哈姆雷特”在代码上同样使用。同时,代码又是只可意会却很难言传的东西。硬件原理,看的多了即使外行人也能指点一下,但是代码只有编写的人才清楚。这也使得很多人认为代码很神秘。 1 代码要多看、多练 在学习编程语言的时候,我们都会有这样的经历:老师在讲每行代码的时候,我们都能听懂,但是要自己写的时候,却一条语句可能都写不出来。所以,代码除了要多看,还要多写。在练习写代码的时候,从最简单的功能开始,要搞懂每一条语句的含义,充分理解编程的思想,搞清楚每一个常用函数的使用方法。其实编程者就是一名经理,每一个函数就是一个工程师,经理就是要发挥每一个工程师的优点去实现一个项目。这就要,经理多和每个工程师谈话,熟悉每个工程师的做事方法、优点以及确定。用好每一个人。 2 编程之前要做好流程框图 拿到一个任务后,千万不要立即开始敲代码,而是要规划一下编程思想和流程,先把程序的流程图画下来。前文说过,实现一个功能的代码方法有很多,最重要的是编程思想,一定要先把自己的编程思想、程序构架梳理好后之后再去填充代码。否则,自己会陷在自己的逻辑里出不来。 3 优化代码、提高执行效率 同样的功能,有的人需要一百行代码,有的人需要七八十行代码,而有的人可能只需要四五十行。有的代码执行效率很高,而有的代码执行效率却很低。功能实现了并不代表任务就结束了,接下来要做的工作就是优化代码,包括优化代码结构、优化变量、减少全局变量等,同时通过测试来验证代码的逻辑防止出现BUG。 编程是一定要动手的,别人讲千百遍也比不上自己的一次动手,代码能看懂却写不出来就是因为动手少,不知道从何下手。 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-10-19 关键词: 嵌入式 代码 程序

  • 漫画算法题:两数之和与三数之和

    前一段时间,小灰分别讲解了两道leecode上的经典算法题: 漫画:如何在数组中找到和为 “特定值” 的两个数? 漫画:如何在数组中找到和为 “特定值” 的三个数? 今天,小灰把这两道题整合起来,并修改了其中的细节问题,感谢大家的指正。 —————  第二天  ————— 什么意思呢?我们来举个例子,给定下面这样一个整型数组(假定数组不存在重复元素): 我们随意选择一个特定值,比如13,要求找出两数之和等于13的全部组合。 由于12+1 = 13,6+7 = 13,所以最终的输出结果(输出的是下标)如下: 【1, 6】 【2, 7】 小灰想表达的思路,是直接遍历整个数组,每遍历到一个元素,就和其他元素相加,看看和是不是等于那个特定值。 第1轮,用元素5和其他元素相加: 没有找到符合要求的两个元素。 第2轮,用元素12和其他元素相加: 发现12和1相加的结果是13,符合要求。 按照这个思路,一直遍历完整个数组。 ———————————— 让我们来具体演示一下: 第1轮,访问元素5,计算出13-5=8。在哈希表中查找8,发现查不到: 第2轮,访问元素12,计算出13-12=1。在哈希表中查找1,查到了元素1的下标是6,所以元素12(下标是1)和元素1(下标是6)是一对结果: 第3轮,访问元素6,计算出13-6=7。在哈希表中查找7,查到了元素7的下标是7,所以元素6(下标是2)和元素7(下标是7)是一对结果: 按照这个思路,一直遍历完整个数组即可。 public class FindSumNumbers {    public static List

    时间:2020-10-15 关键词: 算法 程序

  • CPU执行程序的秘密,藏在了这15张图里

    前言 代码写了那么多,你知道 a = 1 + 2 这条代码是怎么被 CPU 执行的吗? 软件用了那么多,你知道软件的 32 位和 64 位之间的区别吗?再来 32 位的操作系统可以运行在 64 位的电脑上吗?64 位的操作系统可以运行在 32 位的电脑上吗?如果不行,原因是什么? CPU 看了那么多,我们都知道 CPU 通常分为 32 位和 64 位,你知道 64 位相比 32 位 CPU 的优势在哪吗?64 位 CPU 的计算性能一定比 32 位 CPU 高很多吗? 不知道也不用慌张,接下来就循序渐进的、一层一层的攻破这些问题。 正文 图灵机的工作方式 要想知道程序执行的原理,我们可以先从「图灵机」说起,图灵的基本思想是用机器来模拟人们用纸笔进行数学运算的过程,而且还定义了计算机由哪些部分组成,程序又是如何执行的。 图灵机长什么样子呢?你从下图可以看到图灵机的实际样子: 图来源自:http://www.kristergustafsson.me/turing-machine/ 图灵机的基本组成如下: 有一条「纸带」,纸带由一个个连续的格子组成,每个格子可以写入字符,纸带就好比内存,而纸带上的格子的字符就好比内存中的数据或程序; 有一个「读写头」,读写头可以读取纸带上任意格子的字符,也可以把字符写入到纸带的格子; 读写头上有一些部件,比如存储单元、控制单元以及运算单元:1、存储单元用于存放数据;2、控制单元用于识别字符是数据还是指令,以及控制程序的流程等;3、运算单元用于执行运算指令; 知道了图灵机的组成后,我们以简单数学运算的 1 + 2 作为例子,来看看它是怎么执行这行代码的。 首先,用读写头把 「1、2、+」这 3 个字符分别写入到纸带上的 3 个格子,然后读写头先停在 1 字符对应的格子上; 接着,读写头读入 1 到存储设备中,这个存储设备称为图灵机的状态; 然后读写头向右移动一个格,用同样的方式把 2 读入到图灵机的状态,于是现在图灵机的状态中存储着两个连续的数字, 1 和 2; 读写头再往右移动一个格,就会碰到 + 号,读写头读到 + 号后,将 + 号传输给「控制单元」,控制单元发现是一个 + 号而不是数字,所以没有存入到状态中,因为 + 号是运算符指令,作用是加和目前的状态,于是通知「运算单元」工作。运算单元收到要加和状态中的值的通知后,就会把状态中的 1 和 2 读入并计算,再将计算的结果 3 存放到状态中; 最后,运算单元将结果返回给控制单元,控制单元将结果传输给读写头,读写头向右移动,把结果 3 写入到纸带的格子中; 通过 上面 的图灵机计算  1 + 2  的过程,可以发现图灵机主要功能就是读取纸带格子中的内容,然后交给控制单元识别字符是数字还是运算符指令,如果是数字则存入到图灵机状态中,如果是运算符,则通知运算 符单元读取状态中的数值进行计算,计算结果最终返回给读写头,读写头把结果写入到纸带的格子中。 事实上,图灵机这个看起来很简单的工作方式,和我们今天的计算机是基本一样的。接下来,我们一同再看看当今计算机的组成以及工作方式。 冯诺依曼模型 在 1945 年冯诺依曼和其他计算机科学家们提出了计算机具体实现的报告,其遵循了图灵机的设计,而且还提出用电子元件构造计算机,并约定了用二进制进行计算和存储,还定义计算机基本结构为 5 个部分,分别是中央处理器(CPU)、内存、输入设备、输出设备、总线。 这 5 个部分也被称为冯诺依曼模型,接下来看看这 5 个部分的具体作用。 内存 我们的程序和数据都是存储在内存,存储的区域是线性的。 数据存储的单位是一个二进制位(bit),即 0 或 1。最小的存储单位是字节(byte),1 字节等于 8 位。 内存的地址是从 0 开始编号的,然后自增排列,最后一个地址为内存总字节数 - 1,这种结构好似我们程序里的数组,所以内存的读写任何一个数据的速度都是一样的。 中央处理器 中央处理器也就是我们常说的 CPU,32 位和 64 位 CPU 最主要区别在于一次能计算多少字节数据: 32 位 CPU 一次可以计算 4 个字节; 64 位 CPU 一次可以计算 8 个字节; 这里的 32 位和 64 位,通常称为 CPU 的位宽。 之所以 CPU 要这样设计,是为了能计算更大的数值,如果是 8 位的 CPU,那么一次只能计算 1 个字节 0~255 范围内的数值,这样就无法一次完成计算 10000 * 500 ,于是为了能一次计算大数的运算,CPU 需要支持多个 byte 一起计算,所以 CPU 位宽越大,可以计算的数值就越大,比如说 32 位 CPU 能计算的最大整数是 4294967295。 CPU 内部还有一些组件,常见的有寄存器、控制单元和逻辑运算单元等。其中,控制单元负责控制 CPU 工作,逻辑运算单元负责计算,而寄存器可以分为多种类,每种寄存器的功能又不尽相同。 CPU 中的寄存器主要作用是存储计算时的数据,你可能好奇为什么有了内存还需要寄存器?原因很简单,因为内存离 CPU 太远了,而寄存器就在 CPU 里,还紧挨着控制单元和逻辑运算单元,自然计算时速度会很快。 常见的寄存器种类: 通用寄存器,用来存放需要进行运算的数据,比如需要进行加和运算的两个数据。 程序计数器,用来存储 CPU 要执行下一条指令「所在的内存地址」,注意不是存储了下一条要执行的指令,此时指令还在内存中,程序计数器只是存储了下一条指令的地址。 指令寄存器,用来存放程序计数器指向的指令,也就是指令本身,指令被执行完成之前,指令都存储在这里。 总线 总线是用于 CPU 和内存以及其他设备之间的通信,总线可分为 3 种: 地址总线,用于指定 CPU 将要操作的内存地址; 数据总线,用于读写内存的数据; 控制总线,用于发送和接收信号,比如中断、设备复位等信号,CPU 收到信号后自然进行响应,这时也需要控制总线; 当 CPU 要读写内存数据的时候,一般需要通过两个总线: 首先要通过「地址总线」来指定内存的地址; 再通过「数据总线」来传输数据; 输入、输出设备 输入设备向计算机输入数据,计算机经过计算后,把数据输出给输出设备。期间,如果输入设备是键盘,按下按键时是需要和 CPU 进行交互的,这时就需要用到控制总线了。 线路位宽与 CPU 位宽 数据是如何通过地址总线传输的呢?其实是通过操作电压,低电压表示 0,高压电压则表示 1。 如果构造了高低高这样的信号,其实就是 101 二进制数据,十进制则表示 5,如果只有一条线路,就意味着每次只能传递 1 bit 的数据,即 0 或 1,那么传输 101 这个数据,就需要 3 次才能传输完成,这样的效率非常低。 这样一位一位传输的方式,称为串行,下一个 bit 必须等待上一个 bit 传输完成才能进行传输。当然,想一次多传一些数据,增加线路即可,这时数据就可以并行传输。 为了避免低效率的串行传输的方式,线路的位宽最好一次就能访问到所有的内存地址。CPU 要想操作的内存地址就需要地址总线,如果地址总线只有 1 条,那每次只能表示 「0 或 1」这两种情况,所以 CPU 一次只能操作 2 个内存地址;如果想要 CPU 操作 4G 的内存,那么就需要 32 条地址总线,因为 2 ^ 32 = 4G。 知道了线路位宽的意义后,我们再来看看 CPU 位宽。 CPU 的位宽最好不要小于线路位宽,比如 32 位 CPU 控制 40 位宽的地址总线和数据总线的话,工作起来就会非常复杂且麻烦,所以 32 位的 CPU 最好和 32 位宽的线路搭配,因为 32 位 CPU 一次最多只能操作 32 位宽的地址总线和数据总线。 如果用 32 位 CPU 去加和两个 64 位大小的数字,就需要把这 2 个 64 位的数字分成 2 个低位 32 位数字和 2 个高位 32 位数字来计算,先加个两个低位的 32 位数字,算出进位,然后加和两个高位的 32 位数字,最后再加上进位,就能算出结果了,可以发现 32 位 CPU 并不能一次性计算出加和两个 64 位数字的结果。 对于 64 位 CPU 就可以一次性算出加和两个 64 位数字的结果,因为 64 位 CPU 可以一次读入 64 位的数字,并且 64 位 CPU 内部的逻辑运算单元也支持 64 位数字的计算。 但是并不代表 64 位 CPU 性能比 32 位 CPU 高很多,很少应用需要算超过 32 位的数字,所以如果计算的数额不超过 32 位数字的情况下,32 位和 64 位 CPU 之间没什么区别的,只有当计算超过 32 位数字的情况下,64 位的优势才能体现出来。 另外,32 位 CPU 最大只能操作 4GB 内存,就算你装了 8 GB 内存条,也没用。而 64 位 CPU 寻址范围则很大,理论最大的寻址空间为 2^64。 程序执行的基本过程 在前面,我们知道了程序在图灵机的执行过程,接下来我们来看看程序在冯诺依曼模型上是怎么执行的。 程序实际上是一条一条指令,所以程序的运行过程就是把每一条指令一步一步的执行起来,负责执行指令的就是 CPU 了。 那 CPU 执行程序的过程如下: 第一步,CPU 读取「程序计数器」的值,这个值是指令的内存地址,然后 CPU 的「控制单元」操作「地址总线」指定需要访问的内存地址,接着通知内存设备准备数据,数据准备好后通过「数据总线」将指令数据传给 CPU,CPU 收到内存传来的数据后,将这个指令数据存入到「指令寄存器」。 第二步,CPU 分析「指令寄存器」中的指令,确定指令的类型和参数,如果是计算类型的指令,就把指令交给「逻辑运算单元」运算;如果是存储类型的指令,则交由「控制单元」执行; 第三步,CPU 执行完指令后,「程序计数器」的值自增,表示指向下一条指令。这个自增的大小,由 CPU 的位宽决定,比如 32 位的 CPU,指令是 4 个字节,需要 4 个内存地址存放,因此「程序计数器」的值会自增 4; 简单总结一下就是,一个程序执行的时候,CPU 会根据程序计数器里的内存地址,从内存里面把需要执行的指令读取到指令寄存器里面执行,然后根据指令长度自增,开始顺序读取下一条指令。 CPU 从程序计数器读取指令、到执行、再到下一条指令,这个过程会不断循环,直到程序执行结束,这个不断循环的过程被称为 CPU 的指令周期。 a = 1 + 2 执行具体过程 知道了基本的程序执行过程后,接下来用 a = 1 + 2 的作为例子,进一步分析该程序在冯诺伊曼模型的执行过程。 CPU 是不认识 a = 1 + 2 这个字符串,这些字符串只是方便我们程序员认识,要想这段程序能跑起来,还需要把整个程序翻译成汇编语言的程序,这个过程称为编译成汇编代码。 针对汇编代码,我们还需要用汇编器翻译成机器码,这些机器码由 0 和 1 组成的机器语言,这一条条机器码,就是一条条的计算机指令,这个才是 CPU 能够真正认识的东西。 下面来看看  a = 1 + 2 在 32 位 CPU 的执行过程。 程序编译过程中,编译器通过分析代码,发现 1 和 2 是数据,于是程序运行时,内存会有个专门的区域来存放这些数据,这个区域就是「数据段」。如下图,数据 1 和 2 的区域位置: 数据 1 被存放到 0x100 位置; 数据 2 被存放到 0x104 位置; 注意,数据和指令是分开区域存放的,存放指令区域的地方称为「正文段」。 编译器会把 a = 1 + 2 翻译成 4 条指令,存放到正文段中。如图,这 4 条指令被存放到了 0x200 ~ 0x20c 的区域中: 0x200 的内容是 load 指令将 0x100 地址中的数据 1 装入到寄存器 R0; 0x204 的内容是 load 指令将 0x104 地址中的数据 2 装入到寄存器 R1; 0x208 的内容是 add 指令将寄存器 R0 和 R1 的数据相加,并把结果存放到寄存器 R2; 0x20c 的内容是 store 指令将寄存器 R2 中的数据存回数据段中的 0x108 地址中,这个地址也就是变量 a 内存中的地址; 编译完成后,具体执行程序的时候,程序计数器会被设置为 0x200 地址,然后依次执行这 4 条指令。 上面的例子中,由于是在 32 位 CPU 执行的,因此一条指令是占 32 位大小,所以你会发现每条指令间隔 4 个字节。 而数据的大小是根据你在程序中指定的变量类型,比如 int 类型的数据则占 4 个字节,char 类型的数据则占 1 个字节。 指令 上面的例子中,图中指令的内容我写的是简易的汇编代码,目的是为了方便理解指令的具体内容,事实上指令的内容是一串二进制数字的机器码,每条指令都有对应的机器码,CPU 通过解析机器码来知道指令的内容。 不同的 CPU 有不同的指令集,也就是对应着不同的汇编语言和不同的机器码,接下来选用最简单的 MIPS 指集,来看看机器码是如何生成的,这样也能明白二进制的机器码的具体含义。 MIPS 的指令是一个 32 位的整数,高 6 位代表着操作码,表示这条指令是一条什么样的指令,剩下的 26 位不同指令类型所表示的内容也就不相同,主要有三种类型R、I 和 J。 一起具体看看这三种类型的含义: R 指令,用在算术和逻辑操作,里面由读取和写入数据的寄存器地址。如果是逻辑位移操作,后面还有位移操作的「位移量」,而最后的「功能码」则是再前面的操作码不够的时候,扩展操作码来表示对应的具体指令的; I 指令,用在数据传输、条件分支等。这个类型的指令,就没有了位移量和操作码,也没有了第三个寄存器,而是把这三部分直接合并成了一个地址值或一个常数; J 指令,用在跳转,高 6 位之外的 26 位都是一个跳转后的地址; 接下来,我们把前面例子的这条指令:「add 指令将寄存器 R0 和 R1 的数据相加,并把结果放入到 R3」,翻译成机器码。 加和运算 add 指令是属于 R 指令类型: add 对应的 MIPS 指令里操作码是 000000,以及最末尾的功能码是 100000,这些数值都是固定的,查一下 MIPS 指令集的手册就能知道的; rs 代表第一个寄存器 R0 的编号,即 00000; rt 代表第二个寄存器 R1 的编号,即 00001; rd 代表目标的临时寄存器 R2 的编号,即 00010; 因为不是位移操作,所以位移量是 00000 把上面这些数字拼在一起就是一条 32 位的 MIPS 加法指令了,那么用 16 进制表示的机器码则是 0x00011020。 编译器在编译程序的时候,会构造指令,这个过程叫做指令的编码。CPU 执行程序的时候,就会解析指令,这个过程叫作指令的解码。 现代大多数 CPU 都使用来流水线的方式来执行指令,所谓的流水线就是把一个任务拆分成多个小任务,于是一条指令通常分为 4 个阶段,称为 4 级流水线,如下图: 四个阶段的具体含义: CPU 通过程序计数器读取对应内存地址的指令,这个部分称为 Fetch(取得指令); CPU 对指令进行解码,这个部分称为 Decode(指令译码); CPU 执行指令,这个部分称为 Execution(执行指令); CPU 将计算结果存回寄存器或者将寄存器的值存入内存,这个部分称为 Store(数据回写); 上面这 4 个阶段,我们称为指令周期(Instrution Cycle),CPU 的工作就是一个周期接着一个周期,周而复始。 事实上,不同的阶段其实是由计算机中的不同组件完成的: 取指令的阶段,我们的指令是存放在存储器里的,实际上,通过程序计数器和指令寄存器取出指令的过程,是由控制器操作的; 指令的译码过程,也是由控制器进行的; 指令执行的过程,无论是进行算术操作、逻辑操作,还是进行数据传输、条件分支操作,都是由算术逻辑单元操作的,也就是由运算器处理的。但是如果是一个简单的无条件地址跳转,则是直接在控制器里面完成的,不需要用到运算器。 指令的类型 指令从功能角度划分,可以分为 5 大类: 数据传输类型的指令,比如 store/load 是寄存器与内存间数据传输的指令,mov 是将一个内存地址的数据移动到另一个内存地址的指令; 运算类型的指令,比如加减乘除、位运算、比较大小等等,它们最多只能处理两个寄存器中的数据; 跳转类型的指令,通过修改程序计数器的值来达到跳转执行指令的过程,比如编程中常见的 if-else、swtich-case、函数调用等。 信号类型的指令,比如发生中断的指令 trap; 闲置类型的指令,比如指令 nop,执行后 CPU 会空转一个周期; 指令的执行速度 CPU 的硬件参数都会有 GHz 这个参数,比如一个 1 GHz 的 CPU,指的是时钟频率是 1 G,代表着 1 秒会产生 1G 次数的脉冲信号,每一次脉冲信号高低电平的转换就是一个周期,称为时钟周期。 对于 CPU 来说,在一个时钟周期内,CPU 仅能完成一个最基本的动作,时钟频率越高,时钟周期就越短,工作速度也就越快。 一个时钟周期一定能执行完一条指令吗?答案是不一定的,大多数指令不能在一个时钟周期完成,通常需要若干个时钟周期。不同的指令需要的时钟周期是不同的,加法和乘法都对应着一条 CPU 指令,但是乘法需要的时钟周期就要比加法多。 如何让程序跑的更快? 程序执行的时候,耗费的 CPU 时间少就说明程序是快的,对于程序的 CPU 执行时间,我们可以拆解成 CPU 时钟周期数(CPU Cycles)和时钟周期时间(Clock Cycle Time)的乘积。 时钟周期时间就是我们前面提及的 CPU 主频,主频越高说明 CPU 的工作速度就越快,比如我手头上的电脑的 CPU 是 2.4 GHz 四核 Intel Core i5,这里的 2.4 GHz 就是电脑的主频,时钟周期时间就是 1/2.4G。 要想 CPU 跑的更快,自然缩短时钟周期时间,也就是提升 CPU 主频,但是今非彼日,摩尔定律早已失效,当今的 CPU 主频已经很难再做到翻倍的效果了。 另外,换一个更好的 CPU,这个也是我们软件工程师控制不了的事情,我们应该把目光放到另外一个乘法因子 —— CPU 时钟周期数,如果能减少程序所需的 CPU 时钟周期数量,一样也是能提升程序的性能的。 对于 CPU 时钟周期数我们可以进一步拆解成:「指令数 x 每条指令的平均时钟周期数(Cycles Per Instruction,简称 CPI)」,于是程序的 CPU 执行时间的公式可变成如下: 因此,要想程序跑的更快,优化这三者即可: 指令数,表示执行程序所需要多少条指令,以及哪些指令。这个层面是基本靠编译器来优化,毕竟同样的代码,在不同的编译器,编译出来的计算机指令会有各种不同的表示方式。 每条指令的平均时钟周期数 CPI,表示一条指令需要多少个时钟周期数,现代大多数 CPU 通过流水线技术(Pipline),让一条指令需要的 CPU 时钟周期数尽可能的少; 时钟周期时间,表示计算机主频,取决于计算机硬件。有的 CPU 支持超频技术,打开了超频意味着把 CPU 内部的时钟给调快了,于是 CPU 工作速度就变快了,但是也是有代价的,CPU 跑的越快,散热的压力就会越大,CPU 会很容易奔溃。 很多厂商为了跑分而跑分,基本都是在这三个方面入手的哦,特别是超频这一块。 总结 最后我们再来回答开头的问题。 64 位相比 32 位 CPU 的优势在哪吗?64 位 CPU 的计算性能一定比 32 位 CPU 高很多吗? 64 位相比 32 位 CPU 的优势主要体现在两个方面: 64 位 CPU 可以一次计算超过 32 位的数字,而 32 位 CPU 如果要计算超过 32 位的数字,要分多步骤进行计算,效率就没那么高,但是大部分应用程序很少会计算那么大的数字,所以只有运算大数字的时候,64 位 CPU 的优势才能体现出来,否则和 32 位 CPU 的计算性能相差不大。 64 位 CPU 可以寻址更大的内存空间,32 位 CPU 最大的寻址地址是 4G,即使你加了 8G 大小的内存,也还是只能寻址到 4G,而 64 位 CPU 最大寻址地址是 2^64,远超于 32 位 CPU 最大寻址地址的 2^32。 你知道软件的 32 位和 64 位之间的区别吗?再来 32 位的操作系统可以运行在 64 位的电脑上吗?64 位的操作系统可以运行在 32 位的电脑上吗?如果不行,原因是什么? 64 位和 32 位软件,实际上代表指令是 64 位还是 32 位的: 如果 32 位指令在 64 位机器上执行,需要一套兼容机制,就可以做到兼容运行了。但是如果 64 位指令在 32 位机器上执行,就比较困难了,因为 32 位的寄存器存不下 64 位的指令; 操作系统其实也是一种程序,我们也会看到操作系统会分成 32 位操作系统、64 位操作系统,其代表意义就是操作系统中程序的指令是多少位,比如 64 位操作系统,指令也就是 64 位,因此不能装在 32 位机器上。 总之,硬件的 64 位和 32 位指的是 CPU 的位宽,软件的 64 位和 32 位指的是指令的位宽。 絮叨 大家好,我是小林,一个专为大家图解的工具人,如果觉得文章对你有帮助,欢迎分享给你的朋友,也给小林点个「在看」,这对小林非常重要,谢谢你们,我们下次见! 推荐阅读 读者问:小林怎么学操作系统和计算机网络呀? 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-10-12 关键词: CPU 程序

  • Windows XP源码泄露

    来源 | 开源中国(ID:oschina2013) 4chan 论坛的一名用户发帖称 Windows XP 源码已被泄露,并在帖子里面附上了一张正在解压 Windows NT 内核源码的截图,从解压路径来看,被泄露的系统版本是 Windows XP SP1。目前该帖子已被归档,暂不允许回复。 已下载泄露文件的用户提供了如下截图,可以看到整个文件大小为 42.92GB,从目录结构来看,被泄露的内容还包括 Xbox 操作系统源码,以及 Windows NT 5 内核、Windows NT 4 内核和 Windows NT 3.5 内核源码,此外还有 Windows 2000 等其他版本操作系统的源码,其中名为"misc"的文件夹体积最大,总共 31.17GB,占到了整个文件的 70%。 创建并提供种子下载的用户表示,这些文件已在黑客中秘密传播了很多年,他花了大约2个月的时间收集了所有被泄漏的文件,并已经检查了所有的存档,以确保它们的真实性。 这里泄露的是 Windows XP,其实此前已经有微软内核工程师 Axel Rietschin 发表了一篇博客,带大家一窥了 Windows 10 内核的魅力。 Axel 介绍,Windows 10 与 Windows 8.x、7、Vista、XP、2000 和 NT 的代码库是相同的,其中每一代都在之前的基础上进行重大的重构,并增加大量新功能,改进性能和硬件支持,此外还有安全性的提升,同时保持非常高的后向兼容性。 目前在 GitHub 上其实可以找到 Windows 内核研究的泄露副本,虽然这些代码已经过时且很不完整,但它们还是具有很高的研究价值。比如 wrk-v1.2/base/ntos/config 源码实现了一个大名鼎鼎的内核组件配置管理器 Registry,也就是注册表,它在内部称被为 Cm。 Axel 介绍,ntoskrnl.exe 内核大部分是使用 C 编写的,在内核模式下运行的大多数内容也是用 C 编定的,包括文件系统、网络与驱动程序等。其中也包含一些 C++ 代码,而越靠近用户模式、越接近新的源码时,C 的使用变得越来越少,反之 C++ 变多。 具体看一下 Windows 10“DVD”的源码,作者猜测其中 98% 由 C 和 C++ 写就,而 C 占据大比例。 此外,.NET BCL 与一些相关库和框架通常都是用 C# 编写的,“但它们也只不过是带有几座 C++ 小岛的 C 汪洋大海的一栗”,它们来自不同的部门,代码并不属于 Windows 源码树。 作者惊呼:Windows 源码的规模巨大,这是一个真正史诗般的巨型项目。 完整的源码树包含所有代码,如上图所示,测试代码与一起构成“Windows 源码”的所有内容加起来有超过 400 万个文件、50 万个文件夹、大小超过 0.5 TB,其中包含了构成 OS 工作站、服务器和所有版本的工具、相关开发工具包的每个组件的代码。 源码的规模有多恐怖呢?作者估计完全查看这些源码的文件名,并试图理解源码具体是用来干什么的,需要花上一生的时间。他还举了一个例子:有一次,我离开了一个 Git 分支几个星期,当我回来时,已经落在了将近 60 000 次 commit 之后。 —————END————— 喜欢本文的朋友,欢迎关注公众号 程序员小灰,收看更多精彩内容 点个[在看],是对小灰最大的支持! 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-09-29 关键词: 源码 程序

  • 生物识别技术让银行业变得更安全?

    生物识别技术让银行业变得更安全?

    近年来,以客户为中心的金融科技和挑战银行的兴起,推动了对所有参与者(包括现有参与者)更好的客户体验和便利性的需求激增。从即时交易到个性化支出细分,现在银行业经验的每个要素都将是无缝的和个性化的。安全也不例外,但是在处理对银行帐户和高度敏感的个人数据的访问时,必须根据保证程度来衡量便利性。 匹配个人独特的身体特征并消除对广泛的字符数字密码的需求,生物识别(即面部识别)似乎提供了最佳解决方案。 在美国,Chase、HSBC和USAA等主要金融公司允许客户使用Apple的FaceID登录其移动银行应用程序。去年5月,美国银行(BOA)获得了采用多种生物识别技术的面部识别认证系统的专利。 自2016年以来,支付巨头万事达(Mastercard)一直在探索面部识别的使用,当时该公司推出了“自拍照”应用程序,该应用程序使用户可以批准其在线购物以进行即时支付。据报道,这家金融巨头还在探索语音识别技术来批准付款。 显然,银行业在生物识别安全方面已经取得了进展,但是最近关于面部识别技术进行大规模监视的争论促使我们退后一步,重新评估我们对该技术的理解。 对于采用按人脸付款概念的支付公司来说,必须收集、存储和管理大量数据,其中包括生物特征。 关于深度造假绕过面部识别系统的报道表明,坏人可能会释放出一个由错误识别等组成的潘多拉盒子。深度造假正日益成为远程客户身份识别的迫在眉睫的威胁,已经发生了几起引人注目的案件,不法分子利用这种技术推翻了生物识别数据。 虽然生物识别技术旨在抵御影响更传统的验证手段的现有威胁,例如密码、PIN码或图案,但复杂的深度伪造技术可以让攻击者找到面部、语音甚至步态识别的解决方法,从而在升级的安全系统中暴露出新的漏洞。 随着他们竞相采用可加快验证过程并为在线交易增加便利的解决方案,金融行业的公司可能需要退后一步,评估积极的应对措施,以应对这类新出现的威胁。 PenTestPartners的安全研究员KenMunro告诉英国广播公司(BBC),最好的安全措施是稍微“麻烦”一点。 “理想情况下,我希望将面部识别与PIN结合使用。两种系统都有缺陷,但是当您将它们组合在一起时,它们的表现出色。”

    时间:2020-09-26 关键词: 生物识别 银行 程序

  • 部分加拿大移民申请人可以延迟提交生物识别信息

    部分加拿大移民申请人可以延迟提交生物识别信息

    当今,我国的新冠肺炎疫情已经基本上控制住了。然而,全球的新冠肺炎疫情还在不断蔓延,其中就包括加拿大。当前,加拿大移民难民和公民部(IRCC)允许部分加拿大移民因客观原因的,可延期提交生物识别信息。 目前尚不清楚加拿大和境外的生物特征收集服务点什么时候可以正常运行。 服务点和签证申请中心(VAC)的关闭 已影响了成千上万的加拿大永久居留申请人。 这些申请人中有许多之前已经提交过他们的生物特征。 此外,已经有越来越多的永居申请人因不能进行生物识别,而不能满足永居申请的要求。 因此,IRCC采取措施免除某些群体的生物识别,从而避免永居申请程序正常进行。 符合条件的申请人与生效日期 根据IRCC的说法,该政策于2020年9月10日生效,并于2020年9月22日开始实施。它将无限期生效。 无论他们目前在加拿大还是在国外,符合以下两种情况的都可以免除生物特征识别要求: 待定或新的永久居留申请 10年中,在申请永居之前已经提交了生物识别信息 关于生物识别 加拿大不可否认COVID-19对很多申请程序造成了阻碍,因此加拿大实行免除提供的生物识别这一程序。 获得加拿大访客签证,工作或学习许可(美国公民除外),难民或庇护身份,永久居留,访客记录或延长其工作或学习许可的外国公民通常需要进行生物识别。 这些人需要提交其指纹,照片并支付费用。加拿大使用生物识别技术来确认外国人入境时的身份。

    时间:2020-09-26 关键词: 生物识别 信息 程序

  • 让系统发生重大宕机事故的15个方法

    来源| 技术领导力(ID:作者简介 : Mr.K ,知名电商公司技术老K级人物。文出过畅销书,武做过CTO,若非生活所迫,谁愿一身才华。

    时间:2020-09-21 关键词: 互联网 程序

首页  上一页  1 2 3 4 5 6 7 8 9 10 下一页 尾页
发布文章

技术子站

更多

项目外包