[译/中英对照]Dart使用类型安全和空安全来生成更小、更快的代码

577 阅读14分钟

原文链接: medium.com/dartlang/da…

使用类型安全(soundness)和空安全(null safety)来生成更快,更小的代码 Using soundness and null safety to generate faster, smaller code

背景: 什么是类型安全(soundness)?What is soundness?

背景翻译自文章: dart.dev/guides/lang…

类型安全是为了确保程序不会进入某些无效状态。安全的类型系统意味着程序永远不会进入表达式求值为与表达式的静态类型不匹配的值的状态。例如,如果表达式的静态类型是 String ,则在运行时保证在评估它的时候只会获取字符串。Soundness is about ensuring your program can’t get into certain invalid states. A sound type system means you can never get into a state where an expression evaluates to a value that doesn’t match the expression’s static type. For example, if an expression’s static type is String, at runtime you are guaranteed to only get a string when you evaluate it.

Dart 的类型系统,同 Java 和 C#中的类型系统类似,是安全的。它使用静态检查(编译时错误)和运行时检查的组合来强制执行类型安全。例如,如果将 String 分配给 int 是一个编译时错误;如果对象不是字符串,使用 as String对象转换为字符串时,会由于运行时错误而导致转换失败。

Dart’s type system, like the type systems in Java and C#, is sound. It enforces that soundness using a combination of static checking (compile-time errors) and runtime checks. For example, assigning a String to int is a compile-time error. Casting an Object to a string using as String fails with a runtime error if the object isn’t a string.

正文

机器代码的3个屏幕截图:大量代码→较少代码→3行代码。 从Dart 1.24、2.0和2.12(从左到右)中相同的Dart方法生成的代码越来越小。要查看原因(并查看实际生成的代码),请继续阅读。Code generated from the same Dart method in Dart 1.24, 2.0, and 2.12 (left to right) has gotten smaller. To see why (and to see the actual generated code), keep reading.

在过去的几年中,我们加强了Dart的类型系统。原始的Dart语言(Dart 1)具有非安全的可选类型系统(类似于诸如Microsoft的TypeScript或Facebook的Flow之类的带静态类型的JavaScript方言)。Dart 2引入了更严格的安全类型系统。在过去的两年中,我们一直在通过健全的空安全进一步扩展类型系统。We’ve strengthened Dart’s type system over the past few years. The original Dart language (Dart 1) had an unsound, optional type system (similar to typed JavaScript dialects such as Microsoft’s TypeScript or Facebook’s Flow). Dart 2 introduced a stricter, sound type system. Over the past two years, we’ve been working on extending the type system further, via sound null safety.

安全类型系统在使开发人员更放心的同时,还使我们的编译器能够安全地使用类型来优化生成的代码。我们的工具在类型安全的保证下,我们可以通过静态检查,以及在必要时的运行时检查这两者的组合,来确保类型正确。没有类型安全的保证时,类型检查会受到限制,并且静态类型在运行时可能不正确。While a sound type system provides developers with greater confidence, it also enables our compilers to safely use types to optimize generated code. With soundness, our tools guarantee that types are correct via a combination of static and (when needed) runtime checking. Without soundness, type checking can only go so far, and static types may be incorrect at runtime.

实际上,类型安全使我们的编译器可以生成更小,更快的代码,尤其是在AOT(ahead-of-time)模式下,我们可以生成预编译的本地代码。In practice, soundness allows our compilers to generate smaller and faster code, particularly in an ahead-of-time (AOT) setting, where we ship precompiled native code to clients.

一个例子 An example

下面的示例方法演示了类型安全如何对相对简单的代码产生巨大影响:The following example method demonstrates how sound types can have a dramatic impact on relatively simple code:

int getAge(Animal a){ 
  return a.age; 
}

在我们上一个稳定的Dart 1版本(1.24.3)中,此方法生成了26条本机x64指令—并且它是在instrumentation(译?)和基于配置优化(profile-guided optimization)之后才得到这样的结果,这减慢了启动运行时(runtime)做初始化工作的速度。基于Dart 2.12中的健全的空安全,此代码仅映射到3条指令,而无需任何基于配置优化。In our last stable Dart 1 version (1.24.3), this method mapped to 26 native x64 instructions — and that was only after instrumentation and profile-guided optimization, which slowed initial runtime startup. With sound null safety in Dart 2.12, this code maps to just 3 instructions, without any need for profile-guided optimization.

Dart可同时编译为ARM32 / 64和x86 / x64体系结构。在下面的示例中,我们使用x64,但在其他目标上的结果相似。Dart compiles to both ARM32/64 and x86/x64 architectures. In the examples below, we use x64, but results are similar on other targets.

本文末尾显示了示例方法的完整Dart代码和上下文,但下面几个是一些要点:The full Dart code and context for the example method are shown at the end of this article, but here are the key points:

  • 该类Animal包含一个agetype字段int- The class Animal contains a field age of type int.
  • Animal有几个子类(CatDogSnakeHamster)。- Animal has several subclasses (Cat, Dog, Snake, Hamster).
  • 在这些类型中的许多类型在运行时都会调用上面的方法。- The method above is called on many of these types at runtime.

Dart 对象布局 Dart object layout

Dart类Animal在编译为本机(x64)代码时具有简单的布局:The Dart class Animal, when compiled to native (x64) code, has a simple layout:

8个字节的对象标头,后跟8个字节的“age”字段,然后是其他子类字段。

前 8 个字节是提供具体类型信息(即对象的运行时类型)的标头。后8个字节包含该age字段。所有子类都保留(并可能添加到)此结构:在此之后,将保留基本类型的结构来安排任何其他字段。getAge给定方法Animal(或任何子类)的实例,该方法应从8字节偏移量加载该字段并返回它。The first 8 bytes are a header that provides reified type information (that is, the runtime type of the object). The second 8 bytes contain the age field. All subclasses preserve (and potentially add to) this structure: any additional fields are laid out after, preserving the base type’s structure. The getAge method, given an instance of Animal (or any subclass) should load the field from an 8-byte offset and return it.

Dart 1:非安全类型 Unsound types

在Dart 1中,静态类型不是安全的,并且静态类型在编译过程中实际上被忽略了。在运行时,我们不能假定静态类型正确(因此布局符合预期)。对age的访问,可能是到不同的偏移量对应的一个字段,也可能是到一个getter函数并触发进一步的可执行代码,也可能是到一个不存在的字段(触发一个可捕获的运行时错误)。In Dart 1, however, static types weren’t sound and were effectively ignored during compilation. At runtime, we couldn’t assume that the static type was correct (and, therefore, the layout was as expected). The access to age might be to a field at a different offset, to a getter that triggered further executable code, or to a non-existent field (triggering a catchable runtime error).

Dart 1设计为依赖于客户端设备上的即时编译器和虚拟机,它们使用运行时类型信息来优化代码。在此方案中,我们实际上对每种方法进行了两次编译:首先是收集信息,其次是(针对热方法)根据观察到的运行时行为生成更多优化的代码。Dart 1 was designed to rely on a just-in-time compiler and virtual machine on the client device, which optimized the code using runtime type information. In this scheme, we actually compiled each method twice: first, to collect information, and second (for hot methods) to generate more optimized code based on the observed runtime behavior.

Dart 1:首次编译 First compilation

的第一次编译getAge在x64上产生了以下47条指令:The first compilation for getAge produced the following 47 instructions on x64:

2列汇编代码。

请注意,此代码用于确定运行时会发生什么。它不对传递的对象做任何假设,并且有效地执行了与哈希表查找等效的操作,来正确地找到字段,执行getter或抛出错误。Note that this code is instrumented to determine what happens at runtime. It assumes nothing about the passed object and effectively performs the equivalent of a hash table lookup to correctly find the field, execute a getter, or throw an error.

Dart 1:第二次编译 Second compilation

如果遇到代码被频繁重复调用的情况,则会触发第二次优化编译,生成以下 26 条指令:In this case, the code is called repeatedly and triggers a second, optimizing compilation that generates the following 26 instructions:

2列汇编代码(但比以前少)。 大部分代码是蓝色(序言/结尾)或红色(各种检查)。

优化后的代码体积仍然很大。它基于profile信息,发现方法只会遇到调用CatHamsterDog的对象的这样的假设情况相同的时候,优化就可以继续进行。This optimized code is still quite large. It’s based on profile information that found the method was only invoked on instances of Cat, Hamster, and Dog, and is optimized with the assumption that the same will be true going forward.

蓝色的代码是该方法的开头和结尾(用于设置和恢复堆栈)。红色的代码检查了希望的情况:该实例不是null并且是之前见过的一种类型,最后为其他情况调用了一条慢路径(deopt stub)。加粗的代码是load该字段的实际指令。The code in blue is the prologue and epilogue for the method (to set up and restore the stack frame). The code in red checks for the expected cases — that the instance is non-null and is of one of the previously seen types — and invokes a slow path for other cases. The code in bold is the actual work to load the field.

如果后来遇到的行为和之前不同,那么优化后的代码实际上反而会变慢:如果getAge在新实例(例如Snake)上调用该代码,则代码将执行额外的检查,然后就仍然会落在慢路径上。The optimized code may actually be slower if future behavior is different from the past: if getAge is invoked on a new instance (such as a Snake) the code will perform the extra checks but still fall down the slow path.

Dart 1 生成代码的问题 Problems with Dart 1 generated code

上面给出的代码在结构上,与现在Chrome中的JavaScript的V8引擎产生的代码非常相似,而后者几乎具有等效的JavaScript / TypeScript / Flow程序。尽管这种方法(以及相应的生成代码)在许多情况下都可以提供良好的性能,但它并不适合我们刚开始时(尤其是Flutter)针对更广泛的客户端平台,包括对大小和内存更敏感的移动设备:The generated code above is very similar in structure to that produced today by V8, the JavaScript engine in Chrome, when given a more-or-less equivalent JavaScript/TypeScript/Flow program. While this approach (and the corresponding generated code) can give good performance in many scenarios, it wasn’t suitable as we began (with Flutter in particular) to target a broader set of client platforms, including mobile devices more sensitive to size and memory footprint:

  • 首先,客户端编译的成本增加了Dart应用程序的总体覆盖范围。- First, the cost of client-side compilation increased the overall footprint of Dart applications.
  • 其次,两阶段推测性编译的成本不利于应用程序启动。- Second, the cost of two-phase speculative compilation was detrimental to application startup.
  • 第三,iOS上不允许即时编译:我们至少需要针对某些目标使用替代策略。- Third, just-in-time compilation isn’t allowed on iOS: we’d need an alternative strategy for at least some targets.

我们进而改成采用了一种叫AOT(ahead-of-time)编译的方法,但是在Dart 1条件下,它导致了更糟糕的代码。即使进行了复杂的全程序分析,我们也无法始终在编译时确定类型信息,尤其是在应用程序变得更大时。此外,当整个应用程序都被预编译时,投机和去优化的成本(上面的红色代码)变得过高。We shifted instead to an ahead-of-time compilation approach, but with Dart 1 it resulted in considerably worse code. Even with sophisticated, whole-program analysis, we couldn’t always determine type information at compilation time, particularly as applications became larger. In addition, the cost of speculation — the red code above — became prohibitive when the entire application was precompiled.

Dart 2:安全类型 Sound types

在Dart 2中,我们引入了类型安全,使我们能够根据类型信息安全地编译代码,并减少了对性能分析的依赖。使用Dart 2,通过一次AOT(ahead-of-time)编译,我们可以在x64上生成10条指令:With Dart 2, we introduced soundness, which enabled us to safely compile code based upon type information and reduced our reliance on profiling for performance. With Dart 2, on a single ahead-of-time compile, we generate 10 instructions on x64:

代码少了很多,但是仍然有一些蓝色代码(开头/结尾)和红色代码(空检查)。

此代码仍然执行空值检查(红色),如果找到空值,则调用helper方法。 This code still performs the null check (in red) and calls a helper method if null is found.

Dart 2.12:健全空安全 Sound null safety

有了健全空安全,Dart的类型系统会更丰富,我们的编译器可以利用它做更多的事情。编译器可以安全地依赖现在一定不为null的类型,并消除上面红色的代码。在Dart 2.12 Beta中,我们少产生了3条指令:With sound null safety, the type system is richer, and our compiler can leverage that. The compiler can safely rely upon the (now) non-nullable type and eliminate the code in red above. In Dart 2.12 beta, we generate 3 fewer instructions:

一些蓝色代码(开头/结尾),但没有红色代码!无需空检查!

实际上,随着代码变得更加简单,我们还能够简化开头和结尾的指令。在即将发布的稳定版本中,我们将为示例方法仅生成3条指令:In fact, as the code has gotten simpler, we’ve also been able to streamline the prologue and epilogue. In our forthcoming stable release, we’ll generate just 3 instructions for the example method:

甚至比以前更少的蓝色代码(开头/结尾)。

借助健全空安全,我们可以将为此方法生成的代码减少到精髓:load字段。实际上,对此方法的调用将始终是内联的,因为对于编译器而言,内联既是性能又是代码大小的胜利,现在的指令已变得非常简单了。不再需要运行时检查和额外修补代码:更多的处理仅发生在编译时。我们不再需要客户端编译的启动和内存开销。最终我们得到了更小,更快的代码。With sound null safety, we can reduce the generated code for this method to its essence: a field load. In practice, a call to this method will always be inlined, as it’s now trivial for the compiler to see that inlining is both a performance and code size win. Runtime checking and compensation code are no longer necessary: more of the heavy lifting happens at compile time. We no longer need the startup and memory overhead of client-side compilation. As a result, our users get smaller and faster code.

尝试一下!Try it!

我们鼓励您尝试使用健全的空安全。它可以在Dart 2.12中使用,现在可以在我们的beta channel中使用。一旦所有上游依赖库迁移到空安全以后,您就能够迁移自己的软件包和应用程序到空安全。正像这里的例子一样,您可能不需要做很多代码上的更改。We encourage you to try out null safety. It’s available in Dart 2.12, now in our beta channel. Once your upstream dependencies are migrated, you’ll be able to migrate your own packages and applications. As the example here illustrates, you may not need to change too much.

需要注意的是,要获得空安全性的性能优势,需要一个完全迁移到空安全的应用程序。一旦您的应用程序完全迁移,我们的编译器就可以自动利用null安全性来生成更好的,更小的代码。Remember, to get the performance benefits of null safety, you’ll need a fully migrated application. Once your application is fully migrated, our compilers will automatically take advantage of null safety to generate better, smaller code.

PS:代码 The code

这是我用来编译然后生成本文中所有指令代码的完整Dart源码。此处的示例虽然是人为设计的例子,但这种模式(类层次结构中的字段)应该非常普遍。Here’s the full Dart code that I compiled to generate all the code in this article. While the example here is contrived, the pattern — a field in a class hierarchy — is quite common.

int N = 1000000;

class Animal {
  int age = 0;
}

class Cat extends Animal {}

class Dog extends Animal {}

class Snake extends Animal {}

class Hamster extends Animal {}

List<Animal> _animals = [
  new Cat()..age = 1,
  new Hamster()..age = 2,
  new Dog()..age = 3
];
List<Animal> listOfA = [];
void init() {
  for (int i = 0; i < N; ++i) {
    listOfA.add(_animals[i % _animals.length]);
  }
}

int sum() {
  int k = 0;
  for (int i = 0; i < N; ++i) {
    k += getAge(listOfA[i]);
  }
  return k;
}

@pragma('vm:never-inline')
int getAge(Animal a) {
  return a.age;
}

void main() {
  init();
  print(sum());
  print(getAge(listOfA[0]));
}

感谢Kathy Walrath和Michael Thomsen。 Thanks to Kathy Walrath and Michael Thomsen.

保留部分权利 Some rights reserved

参考: