LLVM 项目博客

LLVM 项目新闻和来自实战的细节

使用 clang-cl 和新的 /Zc:dllexportInlines- 标记,Windows 构建速度提升 30%

背景

在为 Clang 添加与 Microsoft Visual C++ (MSVC) 兼容的 Windows 支持的过程中,我们努力确保 dllexport 和 dllimport declspecs 在 Clang 中的处理方式与 MSVC 中相同。

dllexport 和 dllimport 用于指定哪些函数和变量应从当前编译的动态链接库 (DLL) 中外部访问(“导出”),或者应从另一个 DLL 中访问(“导入”)。在下面的类声明中,S::foo() 在构建 DLL 时将被导出


struct __declspec(dllexport) S {
void foo() {}
};

而使用该 DLL 的代码通常会看到这样的声明


struct __declspec(dllimport) S {
void foo() {}
};

表明该函数是在另一个 DLL 中定义的,应该从另一个 DLL 中访问。

通常,相同的声明与 预处理器宏 一起使用,根据是构建 DLL 还是使用 DLL,在 dllexport 和 dllimport 之间切换。

dllexport 和 dllimport 的基本思想很简单,但它们在与 C++ 语言的更多方面交互时,语义会变得更加复杂:模板、继承、不同类型的实例化、带有不同 declspecs 的重新声明等等。有时,语义令人惊讶,但现在我们认为 clang-cl 在大多数情况下都做对了。正如那句老话所说,一旦你熟练地掌握了规则,就可以开始巧妙地打破它们。

dllexport 的一个问题是,对于像上面的 S::foo() 这样的内联函数,编译器必须发出定义,即使它没有在翻译单元中使用。这是因为 DLL 必须导出它,而编译器无法知道是否有其他翻译单元会提供定义。

这非常低效。带有内联成员的 dllexported 类,在头文件中会导致这些成员的定义在包含该头文件的每个翻译单元中发出,无论是直接还是间接。正如我们所知,C++ 源文件最终常常包含很多头文件。这种行为也与非 Windows 系统不同,在非 Windows 系统中,即使在共享对象和动态库中,只有当内联函数定义被使用时才会发出它们,否则不会发出。

/Zc:dllexportInlines-

为了解决这个问题,clang-cl 最近获得了一个新的命令行标志,/Zc:dllexportInlines-(MSVC 使用 /Zc: 前缀表示 语言一致性选项)。基本思想很简单:由于内联函数的定义与其声明一起可用,因此无需从 DLL 中导入或导出它——内联定义可以直接使用。该标志的效果是不将类级别的 dllexport/dllimport declspecs 应用于内联成员函数。在上面的两个例子中,这意味着 S::foo() 不会被 dllexport 或者 dllimport,即使 S 类被声明为这样。

这与 Clang 和 GCC 标志 -fvisibility-inlines-hidden 在非 Windows 上的使用非常相似。对于具有许多内联函数的 C++ 项目来说,它可以显著减少导出的函数集,从而减少共享对象或动态库的符号表和文件大小,以及程序加载时间。

然而在 Windows 上,主要的好处是不必发出未使用的内联函数定义。这意味着编译器需要做的工作要少得多,而且可以减少目标文件大小,这反过来又减少了链接器的负担。对于 Chrome,我们 发现完整构建速度提高了 30%,blink_core.dll 的链接时间缩短了 30%,总的 .obj 文件大小减少了 40%。

.obj 文件大小的减少,再加上之前将链接器切换到使用瘦档案的 lld-link 所带来的 .lib 文件的大量减少,意味着典型的 Chrome 构建目录现在比一年前小了 60%。

(如果 dllexport 内联函数来自预编译头 (PCH) 文件,那么即使没有这个标志,也能获得一些相同的好处。在这种情况下,定义将在构建 PCH 时在目标文件中发出,因此除非使用,否则不会在其他地方发出。)

兼容性

使用 /Zc:dllexportInlines- 是“半 ABI 不兼容”。如果它被用于构建 DLL,内联成员将不再被导出,因此任何使用 DLL 的代码都必须使用相同的标志才能不 dllimport 这些成员。但是,反向场景通常有效:一个没有使用该标志编译的 DLL(例如,使用 MSVC 构建的系统 DLL)可以被使用该标志的代码引用,这意味着引用代码将使用内联定义,而不是从 DLL 中导入它们。

与 -fvisibility-inlines-hidden 一样,/Zc:dllexportInlines- 打破了 C++ 语言的保证,即(即使是内联)函数在程序中都有唯一的地址。当使用这些标志时,内联函数在库内使用和库外使用时将具有不同的地址。

此外,当内联函数(通常会 dllimport)引用 DLL 的内部符号时,这些标志会导致链接错误


void internal();

struct __declspec(dllimport) S {
void foo() { internal(); }
}

通常,对 S::foo() 的引用会使用 DLL 中的定义,该定义还包含 internal() 的定义,但在使用 /Zc:dllexportInlines- 时,会直接使用 S::foo() 的内联定义,导致链接错误,因为找不到 internal() 的定义。

更糟糕的是,如果 internal() 有一个包含静态局部变量的内联定义,那么程序现在将引用该变量与 DLL 中的不同实例


inline int internal() { static int x; return x++; }

struct __declspec(dllimport) S {
int foo() { return internal(); }
}

这会导致非常微妙的错误。但是,由于 Chrome 已经使用了 -fvisibility-inlines-hidden,它有同样的潜在问题,我们认为这不是一个常见的问题。

总结

/Zc:dllexportInlines- 就像 DLL 的 -fvisibility-inlines-hidden,它可以显著减少构建时间。我们很高兴能够在 Windows 上使用 Clang,从而能够从像这样的新功能中受益。

更多信息

有关更多信息,请参见 /Zc:dllexportInlines- 的用户手册

该标志是在 Clang r346069 中添加的,该标志将是预计在 2019 年 3 月发布的 Clang 8 的一部分。它也可以在 Windows Snapshot Build 中获得。

致谢

/Zc:dllexportInlines- 由 Takuto Ikuta 在 Nico Weber 的原型基础上实现。