C++ 中的链接时优化 (LTO)

2025 年 5 月 13 日 | 阅读 12 分钟

引言

在 C++ 开发中,有许多方法可以用来进行性能优化。这对于旨在提供高性能的应用程序尤为重要。然而,有一个特定领域可以得到改进:编译的链接部分,这可以通过 **链接时优化 (LTO)** 来实现。LTO 是一个过程,它允许编译器在跨越翻译单元(即标准源文件)的特定优化过程选择最佳链接时间,并被认为是一种多文件优化技术。因此,LTO 方法在希望减小二进制文件大小并提高应用程序运行时性能的开发人员中很受欢迎。本文将研究 C++ 中的自定义链接,解释其由最初设定的目标定义的用途,它的工作原理,以及它的优点和缺点。

问题陈述

根据规范,C++ 程序是分阶段编译的。按照标准的编译过程,任何存在的源文件 (.cpp 文件) 都被转换为一个 对象 文件 (.o 文件),然后在此最后阶段链接以创建可执行文件或库。然而,这种方法限制了编译器从单个源文件进行优化的能力,因为不同的源文件具有单独编译的视角。因此,这意味着生成的二进制文件将包含一些不必要的代码片段,函数调用中的间隙以及其他残余,如果编译过程是以包容的方式进行的,这些都应该被消除。

这就是链接时优化发挥作用的地方。LTO 能够通过使编译器在链接阶段看到所有文件来实现全程序分析和优化,从而带来更好的运行时性能和更小的二进制文件。

链接时优化是如何工作的?

LTO 基于这样的理念,即每个编译单元对应的最终机器代码直到链接阶段才生成,这使得链接器可以跨所有单元进行优化。以下是在典型的 LTO 启用构建过程中它的工作原理:

  1. 编译阶段:在此阶段,每个源文件都被编译成 IR 而不是本地机器代码。这种中间代码的本质是维护有关可能对优化重要的不同函数和数据结构的高级信息。
  2. 链接阶段:链接器 IR 是所编译单元的 IR,它由多个文件组成。由于链接器熟悉整个代码,它有多种指标可以优化,例如内联函数、消除相似函数,甚至合并来自不同模块的常量。
  3. 最终代码生成:在执行了所有这些优化之后,链接器会输出最终的机器代码,这是一个非常高效的二进制代码,因为它已经经历了许多优化。

链接时优化的优点

链接时优化优势 链接时优化具有一些可以提高 C++ 程序性能和资源的优点。

  1. 跨模块内联:借助链接时优化,可以在一个翻译单元中包含在另一个翻译单元中定义的函数,这些功能可以减少函数调用频率,从而提高性能。
  2. 死代码消除:链接器拥有所有代码的信息,因此,他/她可以删除空文件夹(不包含任何函数或数据),从而最小化二进制文件。
  3. 常量传播和折叠:在链接代码翻译单元后,程序包含需要链接在一起的数据。因此,编译器可以使用生成的常量来生成任何翻译单元的 变量或常量,从而实现整体更好的问题解决。
  4. 改进的代码分析:通过 LTO,编译器可以进行整体代码优化,这使得它能够将许多复杂的跨模块优化结合在一起。
  5. 减小二进制文件大小:当二进制文件包含重复代码且未跨文件优化时,它的大小可能很大。LTO 可以做到这一点,因此二进制文件的大小可以大大减小。

挑战与注意事项

然而,LTO 也并非没有挑战和使用考虑。

  1. 编译时间增加:联合 LTO 会增加链接时间,因为链接需要执行更多的链接时优化。这会减慢构建过程,尤其是在大型代码库中。
  2. 内存使用:LTO 的链接过程需要更多的内存,因为它需要存储和优化整个程序的 IR。在资源有限的系统上,这可能是一个问题。
  3. 兼容性:并非所有构建系统或第三方库都支持 LTO,这可能会限制其在某些项目中的适用性。开发人员可能需要确保整个工具链与 LTO 兼容。
  4. 调试复杂性:调试 LTO 优化后的代码可能具有挑战性,因为内联和代码消除等优化使得映射回原始源代码更加困难。

在 C++ 中启用链接时优化

启用 LTO 的方法因编译器而异。以下是在一些流行的 C++ 编译器中启用它的方法:

  • GCC:在编译和链接阶段都使用 -flto 标志。
  • Clang:与 GCC 类似,在编译和链接阶段都使用 -flto 标志。
  • MSVC (Visual Studio):在 Visual Studio 中,在项目属性下启用“整程序优化 (WPO)”或使用 /GL 和 /LTCG 标志进行编译和链接。

链接时优化有什么用?

LTO 有多种优点,在需要优化的领域最为有用。它们包括:

  • 高性能计算 (HPC):LTO 对于有大量性能要求且单个应用程序长时间运行的应用程序非常有帮助。
  • 嵌入式系统:由于资源总是受限,LTO 可以减小二进制文件的大小并提高效率。
  • 游戏开发:特别是游戏对性能非常敏感,应用 LTO 可以带来更快、更流畅的游戏体验。

示例

让我们以一个快速的例子来说明如何在 C++ 项目中实现链接时优化 (LTO)。我们将开发一个包含 2 个源文件 main.cpp 和 math_utils.cpp 的程序,用于 LTO 演示。演示将展示如何在 GCC 或 Clang 以及 Visual Studio (MSVC) 中启用 LTO 作为示例的一部分。

项目结构

  • main.cpp:它调用 math_utils.cpp 中的函数。
  • math_utils.cpp:一个实现单个函数来计算给定数字平方的源文件。
  • math_utils.h:一个包含单个函数来计算给定数字平方的头文件。

源文件

math_utils.h

此头文件声明了函数。

math_utils.cpp

main.cpp

使用链接时优化构建

使用 GCC 或 Clang

要使用 GCC 或 Clang 启用 LTO,请在编译和链接阶段都使用 -flto 标志。

1. 使用 LTO 启用的标志编译源文件

2. 使用 LTO 启用的标志链接对象文件

3. 运行程序

输出

 
Square of 5 is 25   

在 Windows 上使用 MSVC (Visual Studio)

要在 MSVC 中启用 LTO,请在编译期间使用 /GL 标志,在链接期间使用 /LTCG 标志。

1. 使用启用整程序优化 (LTO) 的标志进行编译

2. 使用 LTO 启用的标志链接对象文件

3. 运行程序

输出

 
Square of 5 is 25   

结论

总而言之,**链接时优化** 是一种强大的技术,通过在链接阶段启用全程序分析来提高 C++ 应用程序的性能和效率。虽然它也带来挑战,例如构建时间增加和内存需求增加,但它在减小二进制文件大小、提高运行时性能和更好的跨模块优化方面的好处使其对许多项目都很有价值。随着现代 C++ 编译器不断发展,LTO 变得越来越易于访问和可靠,使其成为 C++ 开发人员优化其应用程序的一个日益有价值的工具。



 
 

C++ 中的链接时优化 (LTO)

引言

在 C++ 开发中,有许多方法可以用来进行性能优化。这对于旨在提供高性能的应用程序尤为重要。然而,有一个特定领域可以得到改进:编译的链接部分,这可以通过 **链接时优化 (LTO)** 来实现。LTO 是一个过程,它允许编译器在跨越翻译单元(即标准源文件)的特定优化过程选择最佳链接时间,并被认为是一种多文件优化技术。因此,LTO 方法在希望减小二进制文件大小并提高应用程序运行时性能的开发人员中很受欢迎。本文将研究 C++ 中的自定义链接,解释其由最初设定的目标定义的用途,它的工作原理,以及它的优点和缺点。

问题陈述

根据规范,C++ 程序是分阶段编译的。按照标准的编译过程,任何存在的源文件 (.cpp 文件) 都被转换为一个 对象 文件 (.o 文件),然后在此最后阶段链接以创建可执行文件或库。然而,这种方法限制了编译器从单个源文件进行优化的能力,因为不同的源文件具有单独编译的视角。因此,这意味着生成的二进制文件将包含一些不必要的代码片段,函数调用中的间隙以及其他残余,如果编译过程是以包容的方式进行的,这些都应该被消除。

这就是链接时优化发挥作用的地方。LTO 能够通过使编译器在链接阶段看到所有文件来实现全程序分析和优化,从而带来更好的运行时性能和更小的二进制文件。

链接时优化是如何工作的?

LTO 基于这样的理念,即每个编译单元对应的最终机器代码直到链接阶段才生成,这使得链接器可以跨所有单元进行优化。以下是在典型的 LTO 启用构建过程中它的工作原理:

  1. 编译阶段:在此阶段,每个源文件都被编译成 IR 而不是本地机器代码。这种中间代码的本质是维护有关可能对优化重要的不同函数和数据结构的高级信息。
  2. 链接阶段:链接器 IR 是所编译单元的 IR,它由多个文件组成。由于链接器熟悉整个代码,它有多种指标可以优化,例如内联函数、消除相似函数,甚至合并来自不同模块的常量。
  3. 最终代码生成:在执行了所有这些优化之后,链接器会输出最终的机器代码,这是一个非常高效的二进制代码,因为它已经经历了许多优化。

链接时优化的优点

链接时优化优势 链接时优化具有一些可以提高 C++ 程序性能和资源的优点。

  1. 跨模块内联:借助链接时优化,可以在一个翻译单元中包含在另一个翻译单元中定义的函数,这些功能可以减少函数调用频率,从而提高性能。
  2. 死代码消除:链接器拥有所有代码的信息,因此,他/她可以删除空文件夹(不包含任何函数或数据),从而最小化二进制文件。
  3. 常量传播和折叠:在链接代码翻译单元后,程序包含需要链接在一起的数据。因此,编译器可以使用生成的常量来生成任何翻译单元的 变量或常量,从而实现整体更好的问题解决。
  4. 改进的代码分析:通过 LTO,编译器可以进行整体代码优化,这使得它能够将许多复杂的跨模块优化结合在一起。
  5. 减小二进制文件大小:当二进制文件包含重复代码且未跨文件优化时,它的大小可能很大。LTO 可以做到这一点,因此二进制文件的大小可以大大减小。

挑战与注意事项

然而,LTO 也并非没有挑战和使用考虑。

  1. 编译时间增加:联合 LTO 会增加链接时间,因为链接需要执行更多的链接时优化。这会减慢构建过程,尤其是在大型代码库中。
  2. 内存使用:LTO 的链接过程需要更多的内存,因为它需要存储和优化整个程序的 IR。在资源有限的系统上,这可能是一个问题。
  3. 兼容性:并非所有构建系统或第三方库都支持 LTO,这可能会限制其在某些项目中的适用性。开发人员可能需要确保整个工具链与 LTO 兼容。
  4. 调试复杂性:调试 LTO 优化后的代码可能具有挑战性,因为内联和代码消除等优化使得映射回原始源代码更加困难。

在 C++ 中启用链接时优化

启用 LTO 的方法因编译器而异。以下是在一些流行的 C++ 编译器中启用它的方法:

  • GCC:在编译和链接阶段都使用 -flto 标志。
  • Clang:与 GCC 类似,在编译和链接阶段都使用 -flto 标志。
  • MSVC (Visual Studio):在 Visual Studio 中,在项目属性下启用“整程序优化 (WPO)”或使用 /GL 和 /LTCG 标志进行编译和链接。

链接时优化有什么用?

LTO 有多种优点,在需要优化的领域最为有用。它们包括:

  • 高性能计算 (HPC):LTO 对于有大量性能要求且单个应用程序长时间运行的应用程序非常有帮助。
  • 嵌入式系统:由于资源总是受限,LTO 可以减小二进制文件的大小并提高效率。
  • 游戏开发:特别是游戏对性能非常敏感,应用 LTO 可以带来更快、更流畅的游戏体验。

示例

让我们以一个快速的例子来说明如何在 C++ 项目中实现链接时优化 (LTO)。我们将开发一个包含 2 个源文件 main.cpp 和 math_utils.cpp 的程序,用于 LTO 演示。演示将展示如何在 GCC 或 Clang 以及 Visual Studio (MSVC) 中启用 LTO 作为示例的一部分。

项目结构

  • main.cpp:它调用 math_utils.cpp 中的函数。
  • math_utils.cpp:一个实现单个函数来计算给定数字平方的源文件。
  • math_utils.h:一个包含单个函数来计算给定数字平方的头文件。

源文件

math_utils.h

此头文件声明了函数。

math_utils.cpp

main.cpp

使用链接时优化构建

使用 GCC 或 Clang

要使用 GCC 或 Clang 启用 LTO,请在编译和链接阶段都使用 -flto 标志。

1. 使用 LTO 启用的标志编译源文件

2. 使用 LTO 启用的标志链接对象文件

3. 运行程序

输出

 
Square of 5 is 25   

在 Windows 上使用 MSVC (Visual Studio)

要在 MSVC 中启用 LTO,请在编译期间使用 /GL 标志,在链接期间使用 /LTCG 标志。

1. 使用启用整程序优化 (LTO) 的标志进行编译

2. 使用 LTO 启用的标志链接对象文件

3. 运行程序

输出

 
Square of 5 is 25   

结论

总而言之,**链接时优化** 是一种强大的技术,通过在链接阶段启用全程序分析来提高 C++ 应用程序的性能和效率。虽然它也带来挑战,例如构建时间增加和内存需求增加,但它在减小二进制文件大小、提高运行时性能和更好的跨模块优化方面的好处使其对许多项目都很有价值。随着现代 C++ 编译器不断发展,LTO 变得越来越易于访问和可靠,使其成为 C++ 开发人员优化其应用程序的一个日益有价值的工具。