蚊章汇 - Sebastian Sylvan
type: channel
link: https://linuxcn.bwsl.wang/author/547/
description: Sebastian Sylvan
items: 为什么（大多数）高级语言运行效率较慢

为什么（大多数）高级语言运行效率较慢
type: item
date: 2017-03-27T17:27:00+08:00
description: 在近一两个月中，我多次的和线上线下的朋友讨论了这个话题，所以我干脆直接把它写在博客中，以便以后查阅。大部分高级语言运行效率较慢的原因通常有两点：没有很好的利用缓存；垃圾回收机制性能消耗高。但事实上，这两个原因可以归因于：高级语言强烈地鼓励编程人员分配很多的内存。首先，下文内容主要讨论客户端应用。如果你的程序有 99.9% 的时间都在等待网络 I/O，那么这很可能不是拖慢语言运行效率的原因——优先考虑的问题当然是优化网络。在本文中，我们主要讨论程序在本地执行的速度。我将选用 C# 语言作为本文的参考语言，其原因有二：首先它是我常用的高级语言；其次如果我使用 Java 语言，许多使用 C# 的朋友会告诉我 C# 不会有这些问题，因为它有值类型（但这是错误的）。接下来我将会讨论，出于编程习惯编写的代码、使用 普遍编程方法 （ with the grain ） 的代码或使用库或教程中提到的常用代码来编写程序时会发生什么。我对那些使用难搞的办法来解决语言自身毛病以“证明”语言没毛病这事没兴趣，当然你可以和语言抗争来避免它的毛病，但这并不能说明语言本身是没有问题的。回顾缓存消耗问题首先我们先来回顾一下合理使用缓存的重要性。下图是基于在 Haswell 架构下内存延迟对 CPU 影响的 数据：针对这款 CPU 读取内存的延迟，CPU 需要消耗近 230 个运算周期从内存读取数据，同时需要消耗 4 个运算周期来读取 L1 缓冲区。因此错误的去使用缓存可导致运行速度拖慢近 50 倍。还好这并不是最糟糕的——在现代 CPU 中它们能同时地做多种操作，所以当你加载 L1 缓冲区内容的同时这个内容已经进入到了寄存器，因此数据从 L1 缓冲区加载这个过程的性能消耗就被部分或完整的掩盖了起来。撇开选择合理的算法不谈，不夸张地讲，在性能优化中你要考虑的最主要因素其实是缓存未命中。当你能够有效的访问一个数据时候，你才需要考虑优化你的每个具体的操作。与缓存未命中的问题相比，那些次要的低效问题对运行速度并没有什么过多的影响。这对于编程语言的设计者来说是一个好消息！你都不必去编写一个最高效的编译器，你可以完全摆脱一些额外的开销（比如：数组边界检查），你只需要专注怎么设计语言能高效地编写代码来访问数据，而不用担心与 C 语言代码比较运行速度。为什么 C# 存在缓存未命中问题坦率地讲 C# 在设计时就没打算在现代缓存中实现高效运行。我又一次提到程序语言设计的局限性以及其带给程序员无法编写高效的代码的“压力”。大部分的理论上的解决方法其实都非常的不便，这里我说的是那些编程语言“希望”你这样编写的惯用写法。C# 最基本的问题是对 基础值类型 （ value-based ） 低下的支持性。其大部分的数据结构都是“内置”在语言内定义的（例如：栈，或其他内置对象）。但这些具有帮助性的内置结构体有一些大问题，以至于更像是创可贴而不是解决方案。你得把自己定义的结构体类型在最先声明——这意味着你如果需要用到这个类型作为堆分配，那么所有的结构体都会被堆分配。你也可以使用一些类包装器来打包你的结构体和其中的成员变量，但这十分的痛苦。如果类和结构体可以相同的方式声明，并且可根据具体情况来使用，这将是更好的。当数据可以作为值地存储在自定义的栈中，当这个数据需要被堆分配时你就可以将其定义为一个对象，比如 C++ 就是这样工作的。因为只有少数的内容需要被堆分配，所以我们不鼓励所有的内容都被定义为对象类型。引用 值被苛刻的限制。你可以将一个引用值传给函数，但只能这样。你不能直接引用 List<int> 中的元素，你必须先把所有的引用和索引全部存储下来。你不能直接取得指向栈、对象中的变量（或其他变量）的指针。你只能把它们复制一份，除了将它们传给一个函数（使用引用的方式）。当然这也是可以理解的。如果类型安全是一个先驱条件，灵活的引用变量和保证类型安全这两项要同时支持太难了（虽然不是不可能）。这些限制背后的理念并不能改变限制存在的事实。固定大小的缓冲区.aspx) 不支持自定义类型，而且还必须使用 unsafe 关键字。有限的“数组切片”功能。虽然有提供 ArraySegment 类，但并没有人会使用它，这意味着如果只需要传递数组的一部分，你必须去创建一个 IEnumerable 对象，也就意味着要分配大小（包装）。就算接口接受 ArraySegment 对象作为参数，也是不够的——你只能用普通数组，而不能用 List<T>，也不能用 栈数组.aspx) 等等。最重要的是，除了非常简单的情况之外，C# 非常惯用堆分配。如果所有的数据都被堆分配，这意味着被访问时会造成缓存未命中（从你无法决定对象是如何在堆中存储开始）。所以当 C++ 程序面临着如何有效的组织数据在缓存中的存储这个挑战时，C# 则鼓励程序员…