您愿意为类型检查支付多少钱?
这是一个不应该引起争议但无论如何都会引起争议的说法:JavaScript 是一种类型检查语言。
我听过有人把 JavaScript 称为“无类型”(暗示它没有类型的概念)。这很奇怪,因为 JS 最臭名昭著的错误——“undefined 不是函数”——实际上是该语言报告类型不匹配的一个例子。一个所谓的“无类型”语言怎么会抛出一个TypeError
?JS 能感知类型吗?
当然,答案是 JavaScript 是一种类型检查语言:它的类型在运行时进行检查。“JavaScript 是一种类型检查语言”这一说法之所以备受争议,恰恰证明了我们在类型检查方面形成了怪异的“部落主义”。我的意思是,说 JavaScript 在运行时检查类型难道不准确吗?当然准确!Undefined 不是一个函数!
真正无类型语言
汇编语言没有“未定义不是一个函数”。
这是因为它既没有构建时也没有运行时类型检查。它本质上是机器码的人类可读翻译,允许你直接书写,add
而不必手写出与加法机器指令对应的数字。
那么,如果 Assembly 运行时出现类型不匹配的情况,会发生什么呢?如果它不像 JavaScript 那样检查类型并报告不匹配,它会怎么做呢?
假设我写了一个函数,将小写字符串的首字母大写。然后我不小心把这段代码用在了数字上而不是字符串上。哎呀!让我们比较一下在 JavaScript 和 Assembly 中会发生什么。
由于汇编语言没有名为“数字”或“字符串”的低级原语,所以我想更具体一些。对于“数字”,我将使用 64 位整数。对于“字符串”,我将使用 C 语言在 64 位系统上的定义,即“指向以 0 结尾的字节序列的 64 位内存地址”。为了简化示例,该函数将假设字符串是 ASCII 编码的,并且已经以小写字母开头。
我的“将字符串中的第一个字母大写”函数的汇编代码大致执行以下步骤。
- 将我的一个 64 位参数视为内存地址,并从该地址的内存中加载第一个字节。
- 通过将该字节减去 32 来“大写”。(在 ASCII 中,将小写字母的字符代码减去 32 即可将其变为大写。)
- 将结果字节写回原始内存地址。
如果我调用这个函数并传递一个“字符串”(也就是指向字节开头的内存地址),这些步骤将按预期工作。该函数会将字符串的首字母大写。耶!
如果我传递一个普通的整数来调用这个函数……哎呀。我的汇编代码会再次忠实地执行以下步骤:
- 将我的一个 64 位参数视为内存地址,即使它实际上应该是一个整数。从恰好位于该地址的内存中加载第一个字节。这可能会导致段错误(程序立即崩溃,唯一的错误信息是“段错误”),因为尝试读取操作系统不允许此进程读取的内存。让我们继续,假设内存访问恰好被允许,并且程序没有立即崩溃。
- 将我们加载的任何随机数据字节减去 32,将其“大写”。也许这个字节恰好指的是某个学生的考试成绩,我们只是将其减少了 32 分。又或许,我们恰好加载了程序中另一个字符串中间的字符,现在屏幕上显示的不是“欢迎,Dave!”,而是“欢迎,$ave!”,谁知道呢?我们每次运行程序时,加载的数据都会有所不同。
- 将结果字节写回原始内存地址。抱歉,孩子,你的考试成绩现在只低了32分。
希望我们都能认同,“undefined 不是函数”这一说法,相比分段错误和随机内存损坏,无疑是一个显著的改进。运行时类型检查可以有效防止此类内存安全问题,甚至带来更多好处。
字节就是字节,许多机器指令并不区分字节的类型。无论是在构建时还是在运行时,进行某种类型检查都是防止机器错误地解释字节而导致灾难的唯一方法。“字节类型”是将类型检查引入编程的最初动机,尽管它早已超越了这一点。
检查类型的客观成本
在“静态与动态”的食物大战中,很少找到关于客观权衡的讨论,但这个例子实际上说明了这一点。
顾名思义,运行时类型检查就是在运行时进行类型检查!JavaScript 之所以不会像汇编语言那样导致段错误或数据损坏,是因为 JavaScript 会比汇编语言生成更多的机器指令。这些指令会在内存中记录每个值的类型,然后在执行某个操作之前,先从内存中读取该类型,以决定是继续执行操作还是抛出错误。
这意味着在 JavaScript 中,一个 64 位数字通常占用超过 64 位的内存。这既包括存储数字本身所需的内存,也包括存储其类型所需的额外内存。此外,CPU 还需要做更多工作:它必须读取这些额外内存,并在执行给定操作之前检查其类型。例如,在 Python 中,一个 64 位整数占用 192 位(24 字节)的内存。
相比之下,构建时类型检查涉及在构建时进行类型检查!这没有运行时成本,但确实有构建时成本;构建时类型检查的一个客观缺点是你必须等待它。
程序员的时间很宝贵,这意味着程序员因等待构建而受阻的代价也很高。Elm 的编译器构建速度非常快,以至于在NoRedInk ,如果我们选择 TypeScript,我们将付出惨重的“代码编译”生产力代价——更不用说程序员的幸福感、运行时性能或产品的可靠性方面会受到影响。
话虽如此,使用没有构建时检查的语言并不一定会减少等待时间。Stripe的程序员通常会等待 10-20 秒才能执行一个 Ruby 测试,但他们创建的 Ruby 类型检查器能够在这段时间内针对整个代码库提供可操作的反馈。实际上,引入构建时类型检查显然可以减少他们总体的等待时间。
类型检查器的性能优化
构建时和运行时类型检查器都是程序,这意味着它们的性能可以得到优化。
例如,JIT 编译器可以降低运行时类型检查的成本。2020 年的 JavaScript 运行速度比 2000 年的 JavaScript 快了几个数量级,这要归功于其运行时的大量优化。虽然大部分改进都体现在类型检查器之外,但 JavaScript 的运行时类型检查成本也下降了。
相反,在 2000 年至 2020 年期间,JavaScript 的构建时间呈爆炸式增长——这主要得益于类型检查之外。我第一次学习 JavaScript 时(已经快 20 年了,哎呀!)它没有构建步骤。我第一次专业地使用 JS 时,整个项目只有一个依赖项。
如今,仅仅为一个全新的 React 项目安装依赖项就花了我一分钟多的时间——这还不包括开始构建项目本身,更别提进行类型检查了!相比之下,我可以在 1 秒内构建一个用 git 克隆的4000 行 Elm 单页应用程序,包括安装依赖项和进行完整的类型检查。
虽然 JIT 编译器可以提升整体性能,但它们本身也会带来运行时开销,并且无法避免运行时类型检查。Rust 存在的主要原因或许在于提供一种可靠且符合人体工程学的编程语言,避免 JIT 编译器和垃圾收集器带来的运行时开销。
构建时类型检查器也是程序,其性能也可以进行优化。
我们经常把构建时类型检查的性能归入“编译时间”的范畴,但类型检查并非构建速度慢的最大因素。例如,在Rust中,代码生成显然比类型检查对编译时间的影响更大——而且代码生成只有在类型检查完全完成后才会开始。
由于性能优化,一些类型系统基本相同的类型检查器的构建速度比其他检查器更快。例如,Elm 的 0.19.0 版本完全没有改变类型系统,但通过实现某些性能优化(其中包括使部分类型推断所需的时间从O(log(n))缩短为O(1)。
类型系统影响性能
类型系统的设计决策并非无所不用其极!无论是在构建时还是运行时,类型检查的性能都受到类型系统本身特性的限制。
例如,研究人员开发了运行速度非常快的类型推断策略,但这些策略依赖于一些关于类型系统设计的假设。引入某些子类型特性可能会使这些策略失效,因此引入这些特性会降低编译器速度的上限,进而影响其能否提供类型推断。
很容易打趣说“你可以保证在构建时使用 ________ 类型”(用线性类型、细化类型、依赖类型等填空),但这对编译时间的影响却很少被讨论。
如果你的语言明天引入了某个类型系统特性,这会对编译时间产生什么影响?有人开发出快速检查这些类型的方法吗?某个特性需要增加多少价值才能弥补它带来的剑斗停机时间?
运行时类型检查器也需要权衡这些利弊。例如,Python 和 Clojure 的类型系统不同,Ruby 和 Elixir 以及 JavaScript 和 Lua 也存在差异。它们的性能优化程度(例如通过 JIT 编译器)在一定程度上取决于这些类型系统的设计。
由于在运行时检查某些类型系统特性比在构建时检查更快,这些因素共同限制了那些在仅考虑运行时类型检查的类型系统中添加构建时检查的语言的性能。例如,如果 TypeScript 的编译器不需要适应 JavaScript 现有的类型系统,它的运行速度可能会更快。
您愿意支付多少钱?
除了使用像汇编这样真正无类型的语言编写代码外,我们都会在某些地方为类型检查付出代价——无论是在构建时、运行时,还是两者兼而有之。这种代价会根据所进行的性能优化(例如构建时算法改进和运行时 JIT)而有所不同,虽然类型系统的设计选择可能会限制可用的优化,但它们并不会直接影响性能的快慢。
编程需要权衡诸多利弊,在项目初期往往很难预测到后期会出现哪些问题。“构建速度太慢”和“应用程序运行速度太慢”都是很严重的问题,而你选择的编程语言会限制你对这些问题的改进空间。
我们每个人对于这项检查的支付金额以及预期收益的承受能力各不相同。值得认真思考这些权衡利弊,做出明智的决定,而不是仅仅因为熟悉就选择上次使用的技术。
所以下次你开始一个项目的时候,一定要提前考虑这些成本和收益。你愿意为类型检查支付多少钱?
感谢Brian Hicks、Christoph Hermann、Charlie Koster、Alexis King和Hillel Wayne阅读本文草稿。
鏂囩珷鏉ユ簮锛�https://dev.to/rtfeldman/what-would-you-pay-for-type-checking-2pg9