Sorry, you have been blocked

You are unable to access bytcdntp.com

Why have I been blocked?

This website is using a security service to protect itself from online attacks. The action you just performed triggered the security solution. There are several actions that could trigger this block including submitting a certain word or phrase, a SQL command or malformed data.

What can I do to resolve this?

You can email the site owner to let them know you were blocked. Please include what you were doing when this page came up and the Cloudflare Ray ID found at the bottom of this page.

从 C++之父的视角来解锁性能与抽象的关系 | 湖南海纳技工学校

从 C++之父的视角来解锁性能与抽象的关系

当我们谈及编程语言时,往往会想到那些能够快速搭建应用程序的高级语言,它们以其易用性和高效性吸引着众多开发者。然而,在某些特定的场景下,性能成为了至关重要的因素。而 C++,这门以性能著称的语言,正是为了满足这一需求而生。

从 C++之父的视角来解锁性能与抽象的关系

性能与语言
毋庸置疑,C++ 是一门注重性能的语言。如果你不需要性能,尤其当程序的运行时间远远小于写代码花的时间时,像 Python 这样的脚本语言往往是最佳选择。但是,反过来,如果你的应用程序属于计算密集或者内存密集型,特别是,当你的代码需要部署在多台服务器或者移动设备上的场合,使用 C++ 常常就完全值得了。在很接近底层的场合,如果内存和存储资源比较匮乏,C 也常常会是一个很好的选择;但如果你在资源方面不那么捉襟见肘的话,C++ 提供的零开销抽象,会让你的生产力有一个大幅度的提升。
我们回顾一下 C++ 之父 Bjarne Stroustrup 老爷子对“零开销抽象”的解释:
你不用的东西,你就不需要付出代价。
你使用的东西,你手工写代码也不会更好。
换句话说,我们是既要性能,也要抽象。
当然,抽象从来不是没有任何代价的。对于 C++ 而言,至少语言的复杂性,会是这种抽象的代价。
C++ 里“既要……又要……”的地方并不止前面一处。我们还想要初学者友好。我们还想要向后兼容性——几十年前的代码,仍然应该能够正确编译。
显然,这些目标是有矛盾的,不可兼得——你不可能又支持很多抽象功能,又性能高,又对初学者友好,同时还一直保持向后兼容性……
那我们该怎么办呢?

从 C++之父的视角来解锁性能与抽象的关系

洋葱原则
老爷子对此问题的回答是使用洋葱原则。抽象层次就像一个洋葱,是层层嵌套的。在解决问题时,只要可能,你应该使用尽可能高级的抽象机制,利用比较简单的方式来解决问题。只有在因为性能之类的原因需要进一步优化时,我们才应该使用 C++ 提供的高级功能,在使用抽象机制的同时,进行项目相关的特殊定制。当然,人对抽象和性能的理解通常都是有限的,两者都要的话,复杂度通常会很高——因此,这种深度定制的后果往往就会像切洋葱一样,把自己的眼泪熏出来。
拿老爷子的原话:“如果要完成的任务是简单的,那就用简单的方法做;当要完成的任务不是那么简单时,就需要更详细、更复杂的技巧或写法。这就好比你剥下了一层洋葱。剥得越深,流泪就越多。”
根据洋葱原则,在学习 C++ 时,我们不应该从那些琐碎易错的细节学起,自底向上。相反,学习应当自顶向下,先学习高层的抽象,再层层剥茧、丝丝入扣地一步步进入下层。如果一次走太深的话,挫折可能就难免了。

从 C++之父的视角来解锁性能与抽象的关系

系统知识
不过,C++ 是一门系统编程语言,写 C++ 我们几乎肯定会和系统底层打交道(否则可能就没有必要使用 C++ 了)。我们只能说,应当从高层开始学起;而不是说,我们不需要了解系统底层的细节。
一般而言,系统的下面几个方面我们需要较早就接触到,否则很难对性能有很好的理解:
  1. 栈,以及栈内存和堆内存的区别
  2. 多级缓存架构
  3. 多线程和锁
  4. 构建过程
以“栈”为例,这是理解 C++ 里对象生存期的一个关键点。函数的调用信息在栈上,本地变量在栈上,函数返回时所有的本地变量都会被销毁,内存被回收。构造和析构以后进先出的“栈”顺序进行,高效而确定。C++ 里最重要的惯用法,RAII(resource acquisition is initialization),也就顺理成章地出现了。同时,理解了这些之后,为什么返回本地变量的引用或指针是未定义行为,也会非常容易理解。

从 C++之父的视角来解锁性能与抽象的关系

测试与优化
一般而言,指令执行少的代码更快,我们分析算法使用的大 O 表示法也是从这个角度考虑性能的。但实际的项目里,使用这种方式来分析性能可能存在困难。比如:
  • 某些性能相关部分不是我们自己写的(像操作系统提供的接口),没法直接“分析”它的性能,或者分析会很难
  • 缓存架构对性能会有很大的扭曲
  • 系统比较复杂时,我们只关心程序的“热点”在哪里,而“热点”难以预测
  • 某个语言机制的开销很大,超过了大 O 的影响
  • ……
在这个时候,我们就需要自己来进行性能测试,而测试……则非常容易有陷阱。
为了测试性能,我们需要打开优化,而优化本身就可能会影响测试。这有点像量子力学的测不准原理——有没有观察者效果是不同的。如果没有观察者的话, 编译器就可以大胆地做非常激进的优化;但如果有观察者需要查看结果的话,编译器就不能那么肆意妄为了。通过合理安排观察机制,我们才能做到,既能观测到性能 结果、又不对性能产生负面影响。
在很久很久以前,我曾经测到过手工循环对内存清零比使用 memset 函数更快(当前的编译器上你通常不会得到这样的结果了)。这个结果就是我的测试方法有问题造成的。而背后的实际原因是,在缺乏观察者的情况下,C++ 编译器把我的手工循环完全优化没了,而对 memset 则没有优化得那么彻底……
在我的培训课上,在我强调了这些陷阱之后,还是有相当比例的学员,在写测试代码时仍然犯了该类型的错误,导致测试的结果存在各种问题。可见,这是一种非常常见的错误了。编译器能做什么样的优化,我们该如何来避免某些不该发生的优化,这是一个需要持续学习的问题。

从 C++之父的视角来解锁性能与抽象的关系

“学”与“习”
有一种说法是“学编程”没什么用,要“做项目”才有用。——这种说法,有点像学英语的人说,上课学习没什么用,要跟老外多混多说才有用。
这看似有点道理,但其实并不然。跟人说话,只要对方理解了,那就算成功了,你也很容易验证对方是不是真正明白了。一般而言,即使你表达的方式存在问题,真出现大的理解偏差的概率并不那么高。在缺乏直接反馈的场合,比如写作时,上面这种依赖反馈的做法就不可行了。而当你跟计算机沟通时,精确很重要,错一点点都不行。虽然我们也能部分依赖计算机系统的反馈,但要命的是,即使编译通过了,执行结果正确了,都不能说明你的代码没有问题。如果你只使用试错法来写代码的话,那很有可能,只要你修改了一个编译选项,或者增/删了一行代码,执行结果就出问题了。
如果能问题立即暴露出来的话,实际也还好。最怕的就是写出了未定义行为,只在小概率下呈现出来——那调试时真会让人发疯的。
因此,只通过项目实践来写代码完全不可取。这就跟没经过适当的基本学习和训练就去摸武器一样,很可能你把自己炸飞了,还不知道自己是怎么死的。
不过,反过来,只通过书本学、而不进行练习也是完全不可取的。对于任何一种语言,练习都是必需的。学英语需要“听说读写”,学编程语言虽不需要“听说”,但“读写”仍然必不可少。
模仿孔老夫子说一句,习而不学则惘,学而不习则怠。

注:文章内容来源于网络,仅为表达观点所用,如有侵权请后台联系删除。

原创文章,作者:北大青鸟,如若转载,请注明出处:http://news.yy-accp.com/archives/17497