【译】Link Fast: 改善构建和启动时间

908 阅读26分钟

这是新鲜出炉的 WWDC2022 中的一个讲座,主要解释了编译过程中链接这一重要过程,并且分享了苹果在连接器方面的优化进展,其中有一项重要改进其实在 iOS 15 中就已经被网友发现

今天,我想与你分享如何加速链接。我将告诉你苹果为改进链接所做的工作,并帮助你了解在链接过程中实际发生了什么,以便你能提高应用程序的链接性能。

什么是链接

什么是链接?除了自己写代码,你往往也会使用别人的代码,这些代码大多以 library 或 framwork 的形式发布。为了使你的代码能够使用这些库,需要一个链接器。

image.png

目前实际上有两种类型的链接。一种是静态链接,它发生在你构建应用程序时。它可能会影响到应用程序的构建时长,以及应用程序的包大小。另一种是动态链接,它发生在应用程序启动时。它可能会影响应用程序的启动时长。

在这篇文章中,我们将讨论静态和动态链接。但首先,我将定义什么是静态链接以及它的历史背景,并举出一些例子。接下来,我将介绍苹果的静态链接器 ld64 的新特性。最后,有了静态链接的背景,我将详细介绍静态链接的最佳实践。

本文的后半部分将介绍动态链接。我将展示什么是动态链接,它从哪里来,以及在动态链接过程中会发生什么。接下来,我将介绍今年 dyld 的新内容。最后,我将谈一谈你能做些什么来提高你的应用程序的动态链接性能。

最后的最后,我们将用两个新工具来总结,它们将帮助你窥探链接幕后的真相。你将能够看到你的二进制文件里有什么,以及在动态链接过程中发生了什么。

静态链接

什么是静态链接

为了理解静态链接,让我们回到一切开始的时候。 image.png 一开始,程序很简单,只有一个源文件,构建很容易。你只需在你的源文件上运行编译器(cc),它就会产生可执行的程序。但是,我们的程序不可能都写在一个文件里。编辑器如何构建多个原文件,以及每次构建不去重复编译每个函数,是当时的开发人员需要解决的。 image.png 于是开发人员把编译器分成两部分。第一部分将源代码编译到一个新的中间可重定位的对象 .o 文件。第二部分读取可重定位的 .o 文件并产生一个可执行的程序。我们现在称第二部分为 ld ,即静态链接器。所以,现在你知道静态连接是怎么来的了。

随着软件的发展,很快人们就开始使用并分发 .o 文件,但这种方式很麻烦。有人想,"如果我们能把一组 .o 文件打包成一个'库',那不是很棒吗?" 当时,将文件捆绑在一起的标准方法是使用归档工具 ar 。它被用于备份和分发。所以工作流程是这样的。 image.png 你可以将多个 .o 文件 ar 到一个归档文件( .a )中,同时链接器也得到了增强,知道如何直接从归档文件中读取 .o 文件。这对于分享代码是一个很大的改进。在当时,它只是被称为一个库或一个归档文件。今天,我们称它为静态库。

但随后,程序员发现他们的程序越来越大,这是因为这些库中的所有函数都被复制到了程序中,即使这些函数中只有少数被使用。因此,一个聪明的优化被添加进来。与其让链接器使用静态库中的所有 .o 文件,链接器只从静态库中提取实际使用到的 .o 文件。这意味着可以建立一个超级大的 libc.a 静态库,其中包含所有的 C 标准库函数。每个程序都可以与这个 libc.a 链接,但每个程序只得到程序实际需要的 libc 部分。今天我们仍然采用这种模式:选择性加载

选择性加载

为了解释清楚静态库的选择性加载策略,我有一个简单的场景。 image.pngmain.c 中,有一个叫 main 的函数,它调用了一个叫 foo 的函数。在 foo.c 中,有一个 foo ,它调用 bar 。在 bar.c 中,有 bar 的实现,但也有另一个函数的实现,而这个函数恰好没有被使用。最后,在 baz.c 中,有一个函数 baz,它调用一个名为 undef 的函数。

现在我们把每个函数编译成 .o 文件。 截屏2022-06-10 16.14.03.png 你会看到 foobarundef 都没有灰框,因为它们是未定义的。也就是说,这只是一个符号的使用,而不是一个定义。现在,假设你决定将 bar.obaz.o 合并成一个静态库。

接下来,把这两个.o文件和静态库连接起来。让我们来看看实际发生了什么。 image.png 首先,链接器按照命令行顺序对文件进行操作。第一个文件是 main.o。它加载 main.o 并找到了 main 的定义,在符号表中显示。但也发现 main 有一个未定义的 foo 。然后,链接器解析了命令行上的下一个文件,即foo.o ,这个文件添加了 foo 的定义。这意味着 foo 不再是未定义的。但是,加载 foo.o 也为 bar 增加了一个新的未定义符号。现在,命令行上所有的 .o 文件都被加载了,链接器检查是否还有任何剩余的未定义符号。在这种情况下,bar 仍然是未定义的,所以链接器开始查看命令行上的库,看看是否有库可以满足缺少的未定义符号 bar 。链接器发现静态库中的 bar.o 定义了 bar 这个符号。所以链接器从存档中加载bar.o 。在这一点上,不再有任何未定义的符号,所以链接器停止处理库。链接器进入下一阶段,并为程序中的所有函数和数据分配地址。

然后,它把所有的函数和数据复制到输出文件中。恭喜你!你就有了最终的输出程序。注意,baz.o 在静态库中,但没有加载到程序中。它没有被加载是因为链接器有选择地从静态库中加载。这不是显而易见的,但却是静态库的关键所在。

ld64

现在你明白了静态链接和静态库的基本知识。让我们继续讨论最近对苹果静态链接器的改进,也就是 ld64 。应大众要求,我们今年花了一些时间来优化 ld64 。对许多项目来说,新的链接器速度快了一倍。我们是如何做到这一点的?

  • 充分利用多核 CPU 。我们发现了一些可以使用多个内核来并行进行链接器工作的地方。这包括将内容从输入文件复制到输出文件,并行构建 LINKEDIT 的不同部分,以及改变 UUID 计算和编码散列的方式,使其并行进行。

  • 改进了一些算法。我们发现,改用 C++ 的 string_view 对象来表示每个符号的字符串片, exports-trie builder 的性能将得到提升。

  • 使用了最新的密码库,在计算二进制的 UUID 时利用了硬件加速的优势。

  • 改进了其他算法。

C++17 中我们可以使用 std::string_view 来获取一个字符串的视图,字符串视图并不真正的创建或者拷贝字符串,而只是拥有一个字符串的查看功能。std::string_view 比 std::string 的性能要高很多,因为每个 std::string 都独自拥有一份字符串的拷贝,而 std::string_view 只是记录了自己对应的字符串的指针和偏移位置。当我们在只是查看字符串的函数中可以直接使用 std::string_view 来代替 std::string 。

最佳实践

在努力提高链接器性能的同时,我们注意到一些应用程序的配置问题影响了链接时间。接下来,我将谈一谈你可以在你的项目中做些什么来改善链接时间。我将涵盖五个主题。首先,你是否应该使用静态库。然后是三个鲜为人知的配置选项,对你的链接时间有很大影响。最后,我将讨论一些可能让你吃惊的静态链接行为。

考虑是否使用静态库

如果你正打算将源文件构建成静态库,那么你的工程构建时间必然会延长。因为在文件被修改并编译后,整个静态库必须被重建,包括其目录。这包含大量的额外 I/O 。静态库对稳定的代码最有意义。稳定的意思是说,没有被经常改变的代码。你应该考虑将正在开发的代码从静态库中移出,以减少构建时间。

-all_load

上文我们展示了静态链接器的选择性加载特性。但这样做的一个缺点是,它拖慢了链接器的速度。这是因为为了使构建可重复,并遵循传统的静态库语义,链接器必须以固定的、串行的顺序处理静态库。这意味着 ld64 的一些并行化优势不能用于静态库。但是,如果你并不真的需要这种历史行为,你可以使用一个链接器选项来加快你的构建速度。这个链接器选项被称为 all load 。它告诉链接器无脑地加载静态库中的所有 .o 文件。如果你的应用程序加载静态库中的大部分内容,这就很有帮助。使用 -all_load 将允许链接器并行地解析所有的静态库及其内容。

但是,如果你的应用程序使用了一些奇技淫巧,例如它有多个静态库实现相同的符号,并且依赖于静态库的命令行顺序来驱动使用哪种实现,那么这个选项就不适合你。因为链接器会加载所有的实现,而不一定能得到常规静态链接模式下的符号语义。

-all_load 的另一个缺点是,它可能会使你的程序包大小变大,因为未使用的代码现在被加进去了。为了弥补这一点,你可以使用链接器选项 -dead_strip。该选项将使链接器删除无法到达的代码和数据。目前,死代码剥离算法是很快的,并且通常通过减少输出文件的大小来弥补包大小增长。

如果你对使用 -all_load-dead_strip 感兴趣,你应该在有和没有这些选项的情况下对链接器进行对比,看看对你的特定情况是否有收益。

-no_exported_symbols

下一个链接器选项是 -no_exported_symbols 。这里有一个小背景。链接器生成的 LINKEDIT 段中,有一个部分被称为 exports trie,它是一个前缀树(字典树),对所有导出的符号名称、地址和标志进行编码。虽然所有 dylibs 都需要导出符号,但主程序二进制文件通常不需要任何导出符号,因为通常没有什么东西会在主可执行文件中查找符号。如果是这样的话,你可以为应用程序目标使用 -no_exported_symbols 来跳过 LINKEDIT 中 trie 数据结构的创建,这将缩短链接时间。

但是,如果你的应用程序会加载插件,并且插件会链接回主可执行文件,或者你用 xctest 与你的应用程序作为宿主环境来运行 xctest bundles,你的应用程序必须导出所有的符号。这意味着你不能使用 -no_exported_symbols

我的建议是,只有在导出符号特别大的情况下,才去尝试使用该配置。你可以运行这里显示的 dyld_info 命令来计算导出符号的数量。

dyld_ info -exports /path/to/binary

我们看到的一个大型应用程序有大约一百万个导出的符号。而链接器需要两到三秒来构建这么多符号的 trie 结构。因此,添加 -no_exported_symbols 后,该应用程序的链接时间缩短了两到三秒。我将在本讲座后面告诉你更多关于 dyld_info 工具的信息。

-no_deduplicate

下一个选项是:-no_deduplicate 。几年前,我们给链接器增加了一个新的功能:合并具有相同指令但不同名称的函数。事实证明,通过 C++ 模板扩展,你可以得到很多这样的东西。但这是一个昂贵的算法。链接器必须对每个函数的指令进行递归散列,以帮助寻找重复的指令。由于消耗太高,我们限制了这个算法,所以链接器只关注 weak-def 符号。这些符号是 C++ 编译器为未被内联的模板扩展所发出的。

de-dup 是一种包大小优化,而 Debug 构建关注的是构建速度,而不是关于包大小的。所以默认情况下,Xcode 通过向 Debug 配置的链接器传递 -no_deduplicate 来禁用 de-dup 优化。如果你在运行 clang link line 时使用 -O0 ,clang 也会将 no-dedup 选项传递给链接器。

总之,如果你使用 C++ 并且有一个自定义的构建配置,例如,你在 Xcode 中使用非标准的配置,或者你使用一些其他的构建系统,你应该确保你的调试构建添加了 -no_deduplicate 以改善链接时间。我刚才谈到的配置选项是 ld 的实际命令行参数。当使用 Xcode 时,你需要改变你的产品构建设置。在 build settings 中,搜索 "Other Linker Flags"。 image.png 在这里,你可以配置 -all_load-no_exported_symbols-no_deduplicate 等选项。注意这里也有 Dead Code Stripping 选项。

意外情况

现在让我们来谈谈在使用静态库时你可能会遇到的一些意外情况。

第一个意外情况是当你有源代码构建到静态库中,你的应用程序与之链接,而这些代码最终并没有出现在最终的应用程序中。例如,你在某个函数中添加了 attribute used 属性,或者你有一个 Objective-C category。由于链接器的选择性加载特性,如果静态库中的那些对象文件不同时定义一些链接过程中需要的符号,这些对象文件就不会被链接器加载。

另一个意外情况是静态库和死代码剥离。事实证明,死代码剥离可以隐藏许多静态库的问题。通常情况下,缺失的符号或重复的符号会导致链接器出错。但死代码剥离会使链接器从 main 开始对所有的代码和数据进行可及性检查,如果发现缺失的符号是来自不可及的代码,链接器会掩盖缺失符号的错误。同样,如果有来自静态库的重复符号,链接器会选直接择第一个而不会报错。

使用静态库的最后一个意外情况是,当一个静态库被并入多个框架时,这些框架会在孤立的情况下运行良好,但在某些时候,一些应用同时使用了这两个框架,然后砰的一声,你会因为多个定义而得到奇怪的运行时问题。你会看到的最常见的情况是 Objective-C 运行时对同一个类名的多个实例的警告。

总的来说,静态库很强大,但你需要了解它们以避免陷阱。

动态链接

什么是动态链接

现在想一想,随着时间的推移,源代码越来越多,这将如何扩展?我们应该很清楚,随着越来越多的库被使用,最终程序的规模可能会越来越大。这意味着建立该程序的静态链接时间也会随着时间的推移而增加。 image.png 如果我们尝试改变一下之前的链接方式呢?我们把 ar 改为 ld ,现在输出的库是一个可执行的二进制文件。这就是 90 年代动态库的开始。作为一种缩写,我们把动态库称为 dylibs 。在其他平台上,它们被称为 DSO 或 DLLs 。

那么,这里究竟发生了什么?这种改变对可扩展性又有什么帮助呢?其中的关键是:动态库的链接不是将代码从库中复制到最终程序中,而是记录一种承诺image.png 也就是说,它记录了从动态库中使用的符号名称,以及在运行时库的路径。这有什么好处呢?这意味着你的程序文件大小是在你的控制之下。它只包含你的代码,以及它在运行时需要的动态库的列表。你的程序中不再有库的代码副本。

程序的静态链接时间现在与你的代码大小成正比,并且与你链接的动态库数量无关。另外,虚拟内存系统现在可以大显身手了。当它看到在多个进程中使用同一个动态库时,虚拟内存系统将在所有使用该动态库的进程中为该动态库重新使用相同的物理内存页。 image.png 我已经向你展示了动态库是如何开始的以及它们解决了什么问题。但这些"好处"的"代价"是什么呢?

首先,使用动态库的一个好处是,我们加快了构建时间。但代价是,现在你的应用程序启动会更慢。这是因为启动不再只是加载一个程序文件。现在所有的动态库也需要被加载并连接在一起。换句话说,你只是把一些链接成本从构建时间推迟到启动时间。

第二,一个基于动态库的程序会有更多的 dirty page。在静态库的情况下,链接器会将所有静态库中的所有 globals 共同定位到主可执行文件中的同一个 DATA page 中。但是在动态库中,每个库都有自己的 DATA page。

最后,动态链接的另一个代价是它引入了对新事物的需求:动态链接器!

还记得在构建时记录在可执行文件中的那个承诺吗?现在我们在运行时需要一些东西来实现这个承诺,以加载我们的库。这就是 dyld,动态链接器的作用。

dyld

image.png 让我们深入了解一下动态链接在运行时是如何工作的。一个可执行的二进制文件被分割成若干 segment ,通常至少有 TEXT 、 DATA 和 LINKEDIT 。segment 总是操作系统 page size 的倍数。每个 segment 都有不同的权限。例如,TEXT segment 有执行的权限。这意味着 CPU 可以将页面上的字节视为机器代码指令。在运行时,dyld 必须以每个 segment 的权限将可执行文件 mmap() 到内存中,如图所示。 image.png 由于这些 segment 是和内存页对齐的,这使得虚拟内存系统可以直接将程序或 dylib 文件设置为虚拟内存的备份存储(映射)。这意味着在访问这些内存 page 之前,没有任何东西被加载到内存中。当真正访问内存 page 时,会触发一个 page fault,导致系统读取文件的适当子范围,并将其内容填充到需要的内存 page 中。

fix-up

但仅仅是映射是不够的。程序需要以某种方式被“连接”或“绑定”到 dylib 上。为此,我们有一个叫做 fix-up 的概念。 image.png 在图中,我们看到程序得到了指向其使用的 dylib 部分的指针设置。让我们深入了解一下什么是 fix-upimage.png 这是我们的老朋友,mach-o 文件。现在,TEXT 是不可改变的。事实上,在一个基于代码签名的系统中,它必须如此。此时如果调用 malloc() 函数会怎么样呢?在程序构建时,_malloc 的相对地址不可能被知道。静态链接器看到 malloc 是在一个 dylib 中,并转换了调用点。调用点变成了同一 TEXT segment 下的 _malloc$stub,该 stub 是链接器在链接阶段合成的。所以相对地址在构建时是已知的,这意味着可以正确执行 bl 指令。stub 从 DATA 加载一个指针并跳转到该位置。这样做虽然看起来多此一举,但好处是,在运行时不需要改变 TEXT,只有 DATA 被 dyld 所改变。事实上,理解 dyld 的秘诀在于 dyld 所做的所有修复工作都只是 dyld 在 DATA 中设置一个指针。

让我们更深入地了解 dyld 所做的修复工作。dyld 需要 LINKEDIT 中的某些信息,以驱动修复工作的进行。有两种修复方式。

rebase

要解释 rebase 我们需要先了解 ASLR。ASLR 是一种安全机制,它使 dyld 以随机地址加载 dylibs ,这意味着这些内部指针不能在构建时确定。相反,dyld 需要在启动时调整或 rebase 这些指针。在磁盘上,dylib 从地址 0 开始加载,这些指针包含的是它们的目标地址。这样一来,LINKEDIT 需要记录的就是每个重定位的位置。最后,Dyld 只需将 dylib 的实际加载地址添加到每个 rebase 位置,以正确修复它们。

image.png

bind

bind 是一些符号引用。也就是说,它们的目标是一个符号名称而不是一个数字。例如,一个指向函数 malloc 的指针。字符串 _malloc 实际上存储在 LINKEDIT 中,dyld 使用该字符串在 libSystem.dylib 的导出符号中查找 malloc 的实际地址。然后,dyld 将该值存储在由 bind 指定的位置。 image.png

chained fixups

今年我们将公布一种新的编码 fix-up 方式,我们称之为"链式修复"。 image.png 它的第一个优点是使 LINKEDIT 更小。LINKEDIT 之所以变小,是因为新的格式不需要存储所有的修复位置,而只需要存储每个 DATA 页中第一个修复的位置,以及一个导入的符号列表。然后,其余的信息被编码在 DATA 段本身。这种新格式的名称之所以是 "链式修复",是因为修复的位置是"链式"的。LINKEDIT 只记录了第一个 fixup 的位置,然后在 DATA 的 64 位指针地址中,使用几个未被用到的位来记录下一个 fixup 位置的偏移。此外,地址中还有一个位用来标记区分 fixup 是 bind 还是 rebase 。如果它是一个 bind,其余的位是符号的索引。如果它是一个 rebase,剩下的位是目标在库中的偏移。

iOS 13.4 及更高版本中已经存在对 chained fixups 的运行支持。这意味着你今天就可以开始使用这种新格式,只要你的部署目标是 iOS 13.4 或更高版本。而 chained fixups 格式使我们在今年宣布的一项新的操作系统功能成为可能。但要理解这一点,我需要谈谈 dyld 是如何工作的。

page-in linking

Dyld 从主可执行文件开始--比如你的应用程序,解析该 mach-o 以找到依赖的动态库。它找到这些动态库并对其进行 mmap(),然后对每一个动态库进行遍历并解析它们的 mach-o 结构。一旦加载完毕,dyld 会查找所有需要绑定的符号,并在进行修复时使用这些地址。最后,所有修复工作完成后,dyld 自下而上运行初始化程序。 image.png 五年前我们宣布了一项新的 dyld 技术(dyld3)。我们意识到,每次启动您的应用程序时,上面的绿色步骤都是一样的。因此,只要程序和 dylibs 不变,所有绿色部分的步骤都可以在首次启动时被缓存,并在后续启动时重新使用。

今年,我们将宣布更多 dyld 性能改进。我们宣布了一项名为 page-in linking 的 dyld 新功能。和过去 dyld 在启动时一次性统一修正所有 dylibs 不同,内核现在可以在 page-in 的时候去自动修正 DATA 段内的 page。一直以来,内核都可以通过 mmap() 来访问 page。但是现在,如果它是一个 DATA page,内核在访问的同时还会顺便对其进行 fixup。十多年来,我们在 dyld 共享缓存中为操作系统的动态链接提供了一个特殊的 page-in linking。今年,我们将其普及并提供给所有人。这种机制减少了脏内存和启动时间。这也意味着 DATA_CONST 页面是干净的,这意味着它们可以像 TEXT 页面一样被暂时移出内存和重新创建,从而减少内存压力。这个 page-in linking 功能将出现在即将发布的 iOS、macOS 和 watchOS 中。但 page-in linking 只适用于用 chained fixups 构建的二进制文件。这是因为在 chained fixups 中,大部分修复信息将被编码在磁盘上的 DATA 段中,这意味着在 page-in 时内核可以使用这些信息。

有一点需要注意的是,dyld 只在应用启动时使用这一机制。任何后来被 dlopen() 编辑的 dylibs 都不会获得 page-in linking。在这种情况下,dyld 会采用传统的方法,在调用 dlopen 时应用 fixup 。考虑到这一点,让我们回到 dyld 的工作流程图上。 image.png 五年来,dyld 一直在优化上面的绿色步骤,在首次启动时缓存这些工作,并在以后的启动中重复使用它们。现在,dyld 可以不实际进行修复,而让内核在 page-in 时延时进行修复,来优化整个 fixup 步骤。

最佳实践

现在您已经了解了 dyld 的新功能,让我们来谈谈动态链接的最佳实践。你能做些什么来帮助提高动态链接性能?正如我刚才所展示的,dyld 已经加速了动态链接中的大部分步骤。

你可以做的一件事是控制 dylibs 的数量。dylibs 越多,dyld 为加载它们所做的工作就越多。反之,dylibs 越少,dyld 要做的工作就越少。

接下来你可以看一下静态初始化器 static initializers,也就是一定会在 main 之前运行的代码。例如,不要在静态初始化器中做 I/O 或网络操作。任何可能需要超过几毫秒的事情都不应该在初始化器中进行。

正如我们所知,世界正变得越来越复杂,而你的用户想要更多的功能。因此,使用库来管理所有这些功能是有意义的。你的目标是在动态和静态库之间找到平衡点。太多的静态库,你的迭代构建/调试周期就会减慢。另一方面,太多的动态库,启动时间会很慢。但是我们今年加快了 ld64 的速度,所以你的平衡点点可能已经改变了,因为你现在可以使用更多的静态库,或者直接在你的应用程序中使用更多的源文件,并且保持较短的构建时间。

最后,如果允许,更新到一个较新的部署目标可以使用 chained fixups,使你的二进制文件更小,并改善启动时间。

工具

最后,我想让大家注意的是两个新的工具,它们将帮助你窥探链接过程的内部。

dyld_usage

你可以用它来追踪 dyld 正在做什么。该工具仅适用于 macOS ,但你可以用它来追踪你在模拟器中启动的应用,或者 Mac Catalyst 应用。以下是针对 MacOS 上的 TextEdit 运行的一个例子。 image.png 从上面几行可以看出,启动总体上花了 15ms,但得益于 page-in linking ,fixup 只花了1ms。现在绝大部分时间都花在了静态初始化器上。

dyld_info

你可以用它来检查磁盘上和当前 dyld 缓存中的二进制文件。该工具有很多选项,但我将向你展示如何查看导出符号和修复。 image.png 这里的 -fixup 选项显示了 dyld 将处理的所有 fixup 位置和它们的目标。无论文件是旧式 fixup 还是新的链式 fixup ,输出结果都是一样的。这里的 -exports 选项将显示 dylib 中所有导出的符号,以及每个符号从 dylib 开始的偏移。在本例中,它显示的是 Foundation.framework 的信息,这是 dyld 缓存中的 dylib 。磁盘上没有文件,但 dyld_info 工具使用与 dyld 相同的代码,因此可以找到它。

总结

现在你已经了解了静态库和动态库的历史,知道该如何权衡他们。你应该回顾一下你的应用程序是做什么的,并确定你是否已经找到了你的平衡点。

如果您有一个大型的应用程序,并且注意到构建需要花费一些时间来链接,请尝试使用 Xcode 14,它有新的更快的链接器。

如果您仍然想更多地加快您的静态链接,请研究我详述的三个链接器配置选项,看看它们在您的构建中是否有意义,并改善您的链接时间。

最后,你也可以尝试为 iOS 13.4 或更高版本构建你的应用程序,以及任何嵌入式框架,以启用 chained fixups 功能。然后看看你的应用在 iOS 16 上是否更小,启动是否更快。

Link fast: Improve build and launch times