Bjarne访谈:抽象与效率

提升抽象的层面 

Bill Venners(以下简称Bill):我最初是从Borland的一个教学录像“World of C++"”开始学习C++的。在该录像开头的简短片断中,你说你正致力于C++方面的工作是提升编程过程中的抽象层面。 

Bjarne Stroustrup(以下简称BS):是的。 

Bill提升抽象层面意味着什么呢,一个高的抽象层面为什么会有良好 的表现呢? 

BS较高的抽象层面不仅仅在C++中表现良好,在普遍情形下都具有良好表现。我们希望能够在考虑问题的层面上来处理这些问题。如果我们能够以此种方式来处理这些问题,那么在理解这些问题的方式与实现其解决方案的方式之间将不存在鸿沟。我们能够比较容易地理解别人的代码,而不必使自己犹如编译器 一般。    

抽象是一种理解事物的机制。例如,用数学方式来表达一个解决方案则意味着我们真正理解了这一问题。我们不是推出大量的方法针对于特定的情形进行试验。人们经常会仅仅针对一个特定的问题提出解决方案。但是,除非我们试着对问题进行归纳概括并将一个问题看作一类普遍问题的范例,那么我们将会遗漏掉“针对于我们特定问题的解决方案中的”重要部分,而且不可能找到将来对我们具有帮助的概念和一般性 的解决方案。如果某些人具有一条理论,例如有关矩阵操作的一条理论,那么你就可工作于这些概念的层面之上,而且你的代码将变得简洁、清晰且正确的可能性会更大,只需编写少量的代码,而且便于维护。 

我相信提升抽象层面对于所有实际的科学探究都具有基础性的作用。尽管我不认为提升抽象层面是一个有争议的观点,但是由于认为较高抽象层面的代码呈现出不必要的低效率,所有人们有时会认为这是一个存在争议的观点。例如,我两天前收到了一个听过我报告的人 发来的电子邮件,在这次报告中我倡导使用一个具有适当线性代数支持的矩阵库。他说,“使用矩阵库与直接使用数组相比所耗用的资源要多多少?我不敢确定我能承受得起这一耗用。”令他非常惊异的是,我的答案是,“如果你想达到我所指出的效率,你不能直接使用数组。” 

比最快的代码还要快的代码就是根本没有代码。通过对矩阵处理操作的抽象,你可为编译器提供足够的类型信息,从而能使它免除许多操作。如果你正在数组的层面编写代码,除非你比所有的人都聪明,否则你将无法免除这些操作。所以如果你使用的是数组而不是矩阵库,那么你不仅要编写多出十倍的代码,而且还不得不接受一个运行得非常缓慢的程序。通过在我们理解事物的层面进行操作,有时你也可以在我们分析代码的层面执行操作 (在第二种情形下我们是作为编译器,并得到更好的代码)。 

针对于该现象我最钟爱的两个例子为:矩阵乘以向量的操作,其中C++有机会击败Fortran;简单排序 ,其中C++有机会击败C。这两种情形的原因都是因为你已经将程序表达得如此直接、如此清晰,以至于类型系统能够帮助生成更好的代码 。除非你拥有一个适当的抽象层面,否则你将不能实现这一效果。你将遇到其中你的代码变得更加清晰、更加简洁、更加快速的美妙情形。当然这种情形不是时时刻刻都会发生, 不过一旦发生它将是非常得优美。

编程即是理解 

Bill在有关静态类型 和动态类型的争论中,强类型的支持者通常声称,尽管一个动态类型语言能够帮助你快速地检查一个原型,但是为了创建一个健壮的系统你还需一个静态类型语言。相反,我从你的谈论和书籍中所得到有关静态类型的主要消息却是静态类型能够帮助一个优化器更加有效地工作。那么在你的观点中,在C++中以及一般性问题中,静态类型的益处是什么呢? 

BS存在两个益处。首先,我认为在静态类型程序中你能够更好地理解程序。如果我们能够说出“你可执行于一个整数上的特定操作”而且这就是一个整数,那么我们就能够确切获悉现在所发生的一切。 

Bill当你说我们可以获悉正在发生的一切时,你指的是程序员还是编译器? 

BS程序员。 我倾向于更人性化一些。 

Bill使程序员 更人性化一些? 

BS使编译器 更人性化一些。我愿意这么做的部分原因在于它具有诱惑力,部分原因在于我已经编写过编译器。所以作为程序员,我感觉我们能够更好理解一个静态类型语言所发生的事情。 

在一个动态类型语言中,你在执行一个操作时基本上可认为对象的类型在操作处是有意义的,否则你必须在运行期间来处理这一问题。现在,如果你的程序正在运行,而你正坐在一个终端前调试你的代码,那么这可能就是一个查找bugs的好方法。这将具有精确的快速反应时间,而且如果你发现一个操作不能运行,就会发现自己正犹如调试器一样。一切情形将是良好的。如果程序员在工作时,能找到所有的bugs,这一切将非常良好 ,但对于大量的实际程序,你并不能以这种方式找到所有bugs。如果当程序员不在场时bugs显露出来,那么将遇到一个难题。我已经做了大量关于“应当运行在诸如电话交换机 之类的地点中”的程序的工作。在这些环境中,不发生意想不到的事情是至关重要的。在嵌入式系统中同样也是如此。在这些环境中,如果一个bug使人们置身于一个调试器状态,那么没人能够知道该如何应对。 

利用静态类型,我发现能够更容易编写代码,更容易理解代码,更容易理解别人的代码,因为他们想要说明的事情是用语言中具有良好定义的语义进行表达的。例如,如果我指定我的函数带有一个Temperature_reading类型的参数,那么用户就无须再来查看我的代码以便决定我需要何种类型的对象,只要查看一下接口就可以解决问题。如果用户给我一个错误类型的对象,那么我也无需检查,因为编译器将会拒绝Temperature_reading类型以外的任何参数。无需进行任何转型操作我就可直接将我的参数用作一个Temperature_reading。另外 我还发现开发静态类型接口是一个很好的习惯。如果一定要使我考虑什么是本质的东西,而不是仅仅将所有东西用作参数和返回值,从而变得似是而非,那就是希望调用者和被调用者能够达成一致并都编写必要的运行期检查。 

正如Kristen Nygaard所说的那样,编程即是理解(programming is understanding),意思是,如果你对一些事情不理解,你就不能对其进行编程,对其进行编程就应当获取对其的理解。这也是我的《The C++ Programming Language》第三版的前言。这是非常基本的东西,在你知道你拥有一个整数vector而不是一个指向对象的指针的地方,将会更容易读懂一块代码。当然,你可询问对象是否为一个vector,如果它是一个vector,你可询问它是否容纳一些整数、一些字符串 或一些图形。如果你需要此类容器,你可创建它们,但是我认为你应当选择同类的vectors,即容纳某一特定类型的vectors而不是一个容纳常规对象的常规集合。为什么呢?它实际上是那些需要静态检查接口的参数的一个变体。如果我拥有一个vector<Apple>,那么我就知道它的元素是Apples。我无需将一个Object转型为一个Apple来使用它,而且我也不用担心你将我的vector用作一个vector<Fruit>,并将一个Pear塞入其中,或将其用作一个vector<Object>,并将一个HydraulicPumpInterface塞入其中。我认为到现在为止这都已相当容易理解。即使JAVAC#都准备开始提供支持这一功能的 通用机制。 

另一方面,你不能创建完全是静态类型的系统,因为这样一来你必须将所编译的整个系统部署为一个从不发生变化的单元。诸如虚拟函数的更多动态 技术的益处是你能够链接那些“为了完成静态类型检查,你所知信息还不充分的”东西。然后,你可使用你所知的任何初始接口来检查这一系统所拥有的接口。你可询问一个对象一些问题,然后在基于答案的基础上开始使用它。问题是遵循某一系列的,“你是遵循Shape接口的东西吗?”如果你得到的答案是yes,你就可以将Shape的有关操作运用在它上面。 如果你得到的答案是no,你会说“哎呀,”并对其进行处理。有关与此的C++机制是dynamic_cast。使用dynamic_cast的“提问方式”与动态类型语言的相反,在后一种情形中,你倾向于马上就开始运用操作,如果它不运作的话,你就说“哎呀。”通常这一令人惊讶的情形发生于计算与对象为你所知那一刻的中间。后面一种令人惊讶的情形则比较难以应对。 

另外,针对于编译器优化方面的益处也是巨大的。动态类型、静态类型以及决议操作之间的差异很容易达到50倍。当我谈论到效率时,我喜欢谈论倍数,因为由此你能够很容易地看出差异所在。 

Bill系数? 

BS当你接触到百分数,10%、50%等时,你可能会争论效率是否重要,相应的解决方案也许是今后更先进的计算机而不再是优化。但是针对动态和静态,我们是在讨论倍数:3倍、5倍、10倍、50倍。我认为那些需要在巨型计算机上处理的实时问题的一点点区别都很重要,其中一个为10的系数甚至是一个2倍的系数都关系着成功和失败的差异。 

Bill你 并不仅仅在讨论有关动态与静态方法调用,你还讨论了优化,对吗?优化器具备更多的信息并能够执行一项更好的任务。 

BS是的。 

Bill这是如何运作的呢?优化器如何使用类型信息来执行更好的优化操作呢? 

BS让我们看一个非常简单的情形。C++具有静态和动态绑定成员函数。如果你执行了一个虚拟函数调用,那么即为一个间接函数调用。如果是一个静态绑定,那么将是一个极为普通的函数调用。如今一个间接函数调用耗费的资源要比直接调用多出25%。这不是一笔很大的开销。如果它是一个“在整数上执行小于比较一类操作的”极小函数,那么一个函数的相对耗费将是巨大的,因为有更多的代码需要被执行。你不得不去执行函数的前奏内容、执行操作、执行后续内容(如果存在此类事情的话),你不得不将更多的指令加载到机器中。你分裂了传递路径,尤其当它是一个间接函数调用时。所以针对于如何执行小于比较你将得到一个1030的系数。如果这一差异发生于一个关键的内部循环中,那么该差异将变得至关重要。这就是C++排序如何击败C排序的。C排序将一个函数传递为间接调用。C++版本则传递一个函数对象,其中你能够拥有一个退化为“小于比较”的静态绑定内联函数。 

未成熟的优化或是谨慎的优化 

BillC++文化总是与效率相连。是否存在大量未成熟的优化呢?我们如何知晓早期未成熟的优化与早期谨慎的优化之间的差异呢? 

BSC++社群的某些部分 关心效率,我认为其中一些具有充分的理由,而其他的则仅仅是因为他们不是很理解。他们对于不是十分确切的低效感到担心。但是,肯定存在对效率的关注,我认为有两种看此问题的方法。我看待效率的方法是这样的:我希望知道我的抽象能够以一种合理的方式与机器相匹配,而且我希望拥有我自己能够理解的抽象。 

如果我想进行线性代数运算,我需要一个矩阵类,如果我想进行绘图,那么我需要一个绘图类,如果我想进行字符串操作,我就需要一个字符串类。我首先要做的事情就是将抽象的层面提升到一个合适的层面。我使用这些非常简单的例子,是因为它们是最为常见也最容易讨论的。下面需要注意的事情就是在我不需要的地点不拥有N2N3算法。如果我拥有本地化的信息,我就不会再到Web上搜寻信息。如果我在内存中拥有缓存的信息,那么我就不用到硬盘上去找。我看到人们使用建模工具最终以写两次硬盘而将两个字段全部写入到一个记录中告终。为了避免此类算法,我认为这是一个谨慎的前端设计层面的优化,即你应当关注的一类事情。

现在,一旦你拥有一个位于适当高度的抽象层面的适当模型世界,那么你就可以开始进行优化,而且此类后续的优化也是适当的。我不喜欢的是,那些担忧高级特性和担忧抽象的人们开始时就使用语言的一组非常有限的子集 或者为了支持自己手写的代码而避免使用设计良好的库。他们在能够处理对象的地方处理字节。由于担心一个vector或是一个map类过于昂贵而采用数组。那么,他们最终只能编写更多的代码,而这些代码后来还不能被人理解。因为在任何大型系统中你都要对其进行分析并找出你使其出错的地点,所以这就成了一个问题。 

你另外也会试着去拥有较高层面的抽象以便你能够检测具体的事情。如果你使用一个map,可能会发现它 代价过于高昂,这是极有可能的。如果你拥有一个带有一百万个元素的map,那么很可能它会慢下来。它是一个红黑树。在许多情形下,如果你需要进行优化,都可用一个 哈希表来替换一个map。如果你只拥有一百个元素,那么将不会有任何区别。但如果有一百万个元素,那么差别就大了。 

现在,如果你是在最低层面编写一切代码,即使一次,那么你也将不知道自己拥有的是什么。可能你知道你所拥有的数据结构是一个map,但更为可能的是它是一个类似map的特殊数据结构。一旦你知道这一特殊数据 结构运作不正确,你怎样才能知道应该用哪一个数据结构来替换它呢?由于你工作于如此低的层面,所以很难知道该如何操作。而最后,如果你编写了一个特殊的数据结构,你可能将操作遍布于你的整个程序。这对于一个随机数据结构并不是不常见的。并没有一组你用以对其进行操控的固定操作,有时“为了追求效率”,数据是直接从用户代码进行访问。在此种情形下,你的编译器并不会告诉你瓶颈所在的地点,因为你将代码散布于整个程序之中。从概念上讲,瓶颈隶属于某些东西,但你对其并没有概念,或你不能直接表达这一概念。因此你的工具并不能向你展示是由这一概念引起的问题。如果某一东西不是直接位于代码中,那么没有工具能够根据其适当的名字来向你说明关于它的信息。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值