Clangd性能优化的优秀例子


文档摘要

为了获得平滑的IDE体验,并能够在没有明显延迟的情况下提供准确的结果,这是一个具有挑战性的任务。实现这一体验的一种方式是通过编译器性能优化,良好的导航可以通过解析良好的源代码来实现。Clangd提供了许多性能优化的优秀例子,将进行一些详细探讨。 正如第4行所看到的,打开文档时,导航支持需要AST作为基本数据结构,因此需要使用Clang前端来获取。此外,当文档发生修改时,还需要重建AST。文档修改是开发者的常见活动,如果总是从头开始构建过程,将无法提供良好的IDE体验。 为了深入了解用于加快修改文档的AST构建的速度,检查一个简单的C++程序: \#include \ int main() std::cout \ 图8.37:C++程序:helloworld.

为了获得平滑的IDE体验,并能够在没有明显延迟的情况下提供准确的结果,这是一个具有挑战性的任务。实现这一体验的一种方式是通过编译器性能优化,良好的导航可以通过解析良好的源代码来实现。Clangd提供了许多性能优化的优秀例子,将进行一些详细探讨。

正如第4行所看到的,打开文档时,导航支持需要AST作为基本数据结构,因此需要使用Clang前端来获取。此外,当文档发生修改时,还需要重建AST。文档修改是开发者的常见活动,如果总是从头开始构建过程,将无法提供良好的IDE体验。

为了深入了解用于加快修改文档的AST构建的速度,检查一个简单的C++程序:

#include <iostream>

int main() std::cout << "Hello world!" << std::endl; return 0;

图8.37:C++程序:helloworld.cpp

该程序有六行代码,但结论可能会产生误导。#include指令插入了很多的代码。如果我们运行Clang并使用-E命令行选项,我们可以估算预处理器插入的代码量,并计算行数,如下所示:

图8.38:预处理后的程序行数

其中<…>是克隆llvm-project文件夹的目录;参见图1.1。

解析的代码超过了36,000行代码。这是一种常见模式,大部分需要编译的代码都是从包含的头文件中插入的。文档开头部分,包含#include指令的部分被称为前缀。

值得注意的是,前缀的修改是可能的但很少见,例如,当插入一个新的头文件时。大多数修改位于前缀之外。

性能优化的主要思想是缓存前缀AST并在任何修改文档的编译中重用它。

Clangd中的性能优化涉及一个两部分的编译过程。在第一部分,包含所有包含的头文件的前缀被编译成预编译头;参见第10.2节,预编译头。然后,在编译过程的第二阶段使用这个预编译头来构建AST。

这个复杂的过程作为性能优化,特别是在用户需要重新编译文件时。虽然编译时间的大部分花在了头文件上,但这些文件通常不会频繁修改。为了解决这个问题,Clangd在预编译头文件中缓存了头文件的AST。

当在头文件之外进行修改时,Clangd不需要从头开始重建它们。相反,可以重用头文件的缓存AST,显著提高编译性能并减少处理头文件时重新编译所需的时间。如果用户修改影响了头文件,那么整个AST应该被重建,在这种情况下会发生缓存缺失。值得注意的是,头文件的修改并不像修改主源代码(不在包含的头文件中)那么常见。因此,可以期望普通文档修改的缓存命中率相当高。

预编译头可以存储在磁盘上的临时文件中,也可以驻留在内存中,这也可以视为一种性能优化。

缓存的前缀是一个强大的工具,显著提高了Clangd处理用户修改的文档的能力。另一方面,始终考虑涉及前缀修改的边缘情况。前缀可以通过两种主要方式进行修改:

  • 显式:当用户显式修改前缀时,例如,通过在其中插入新的头文件或删除现有的一个

  • 隐式:当用户隐式修改前缀时,例如,通过修改预编译头中包含的头文件

第一个可以通过影响前缀所在范围的"textDocument/didChange"通知轻松检测到。第二个则比较棘手,Clangd应该监控包含头文件的修改,以便正确处理导航请求。

Clangd还进行了一些修改,旨在使前缀编译更快。其中一些修改需要Clang中特定的处理。

在Clangd中,可以对函数体进行一种有趣的优化。函数体可以被视为主要索引的重要组成部分,包含用户可以点击的符号,例如获取符号的定义。这主要适用于IDE中可见的函数体。另一方面,许多函数及其实现(函数体)在包含的头文件中隐藏对用户可见。因此,用户无法请求这些函数体中的符号信息。这些函数体对编译器可见,因为它解析包含指令并从指令中解析头文件。考虑到复杂项目可能具有许多依赖关系,因此可能会包含许多用户打开的文档中的头文件,编译器花费的时间可能会相当长。一个明显的优化是,在解析预编译头中的头文件时跳过函数体。这可以通过使用一个特殊的前端选项来实现:

/// FrontendOptions - Options for controlling the behavior of the
frontend. class FrontendOptions ... /// Skip over function bodies to
speed up parsing in cases where you do not need /// them (e.g., with
code completion). unsigned SkipFunctionBodies : 1; ... ;

图8.39:clang/Frontend/FrontendOptions.h中的FrontendOptions类

Clangd在以以下方式构前序时使用此选项:

std::shared_ptr<const PreambleData> buildPreamble(PathRef FileName,
CompilerInvocation CI, const ParseInputs &Inputs, bool StoreInMemory,
PreambleParsedCallback PreambleCallback, PreambleBuildStats *Stats) ...
// Skip function bodies when building the preamble to speed up building
// the preamble and make it smaller.
assert(!CI.getFrontendOpts().SkipFunctionBodies);
CI.getFrontendOpts().SkipFunctionBodies = true; ... auto BuiltPreamble =
PrecompiledPreamble::Build(...); ... // When building the AST for the
main file, we do want the function // bodies.
CI.getFrontendOpts().SkipFunctionBodies = false; ... ;

图8.40:clang-tools-extra/clangd/Preamble.cpp中的buildPreamble

Clangd使用前端选项在构建预编译头时跳过头文件中的函数体,在构建主文件的AST之前禁用它;参见图8.40中的第10行和第16行。

这种优化可以显著提高复杂C++源文件的文档准备时间(即打开的文档准备好响应用户的导航请求时)。

尽管这里讨论的性能优化为Clangd的效率提供了有价值的见解,但重要的是要记住,Clangd还采用了多种其他技术来确保其可靠性和速度。Clangd是一个极好的平台,用于实验和实施各种优化策略,使其成为一个多功能的环境,适用于性能提升和创新。


发布者: 作者: 转发
评论区 (0)
U