减小编译bin文件大小_程序员效率分享:加速C ++编译

更多互联网新鲜资讯、工作奇淫技巧关注原创【飞鱼在浪屿】(日更新)

df9241fcda8eaec45a7e120e175b704d.png

这篇文章将介绍一些用于加速C ++编译的源代码级技术。它不会谈论C ++外部的事情,例如购买更好的硬件,使用更好的构建系统或使用更智能的链接器。它也不会谈论可以发现编译瓶颈的工具。


C ++编译模型概述

从C ++编译模型的简介开始,为稍后将介绍的一些技巧提供铺垫。

C ++二进制文件的编译分为3个步骤:

  1. 预处理
  2. 汇编
  3. 链接

预处理

第一步是预处理。这期间,预处理器需要一个.cpp文件,并解析它,寻找预处理器指令,如#include,#define,#ifdef,等

// tiny.cpp#define KONSTANTA 123int main() {    return KONSTANTA;}

这个例子包含一个预处理程序指令#define。以后出现的任何情况KONSTANTA都应替换为123。通过预处理器运行文件将导致如下所示的输出:

$ clang++ -E tiny.cpp# 1 "tiny.cpp"# 1 "" 1# 1 "" 3# 383 "" 3# 1 "" 1# 1 "" 2# 1 "tiny.cpp" 2int main() {    return 123;}

我们可以看到,return KONSTANTA的KONSTANTA部分已被替换123。还看到编译器留下了很多其他注释,这里对此并不太关心。

预处理器模型的最大问题是该#include指令的字面意思是“在此处复制粘贴此文件的所有内容”。当然,如果该文件的内容包含其他#include指令,则将打开更多文件,将其内容复制过去,进而,编译器将需要处理更多代码。也就是说,预处理通常会明显增加输入的大小。

以下是使用流的C ++中的简单“ Hello World”。

// hello-world.cpp#include int main() {    std::cout << "Hello World";}

预处理后,该文件将有28115 行用于下一步(编译)进行处理。

$ clang++ -E hello-world.cpp | wc -l28115

汇编

预处理文件后,将其编译为目标文件。目标文件包含要运行的实际代码,但是如果没有链接就无法运行。原因之一是目标文件可以引用它们没有其定义(代码)的符号(通常是函数)。例如,如果.cpp文件使用已声明但未定义的函数,则发生这种情况:

// unlinked.cppvoid bar(); // 可能任何其他位置定义void foo() {    bar();}

您可以使用nm(Linux)或dumpbin(Windows)在已编译的目标文件中查看其提供的符号以及所需的符号。如果我们查看unlinked.cpp文件的输出,则会得到以下信息:

$ clang++ -c unlinked.cpp && nm -C unlinked.o                 U bar()0000000000000000 T foo()

U表示该符号未在此目标文件中定义。T表示该符号在text / code部分中,并且已将其导出,这意味着其他对象文件可以foo从this 获得unlinked.o。符号也可能存在于目标文件中,但不可用于其他目标文件。此类符号用标记t。


链接

在将所有文件编译成目标文件之后,必须将它们链接到最终的二进制文件中。在链接期间,所有各种目标文件都以特定格式(例如ELF)拼凑在一起,并使用由不同目标文件(或库)提供的符号地址来解析对目标文件中未定义符号的各种引用。


6473c0d21a0cba945f9e57c0579593f6.png

接下来开始研究加快代码编译速度的各种方法。

#include 少用

包含文件通常会带来很多额外的代码,然后编译器需要对其进行解析和检查。因此,加快代码编译速度的最简单方法(通常也是最有效方法)#include较少文件数量。减少头文件特别有好处,因为它们很可能会被其他文件间接包含进来,从而扩大了改进的影响。

最简单的方法是删除所有未使用的include。未使用的include可能不会经常发生,但是有时它们在重构过程中会被遗忘,使用IWYU之 类的工具可以(https://include-what-you-use.org/)简化操作。


包含头文件的成本

下表显示了Clang编译包含一些stdlib标头的文件所需的时间。

25bb1031cb4d09fb107b7276b733cd71.png

第一行显示了编译一个完全空的文件所需的时间。这是编译器启动,读取文件以及不执行任何操作所需的基准时间。第二行看出,即使没有实际使用,仅包括在内就增加了57 ms的编译时间。还可以看到,include 的成本是的两倍多,include 的成本几乎和相同。

包含多个头文件的结果比较有趣,因为多个头文件组合并不是单独编译每个头文件的成本相加。原因很简单:它们的内部包含有重叠。最极端的情况是+ ,因为基本上是衍生的几种std::exception类型,所以这里和成本一样。

  • 即使不使用头文件中的任何内容,也仍然需要为此付出成本。
  • include成本既不能简单地相加,也不能相减。

forward declaration/前向声明/预先声明

通常,当提到一个类型时,只需要知道它的存在而不必知道它的定义。通常的做法是创建类型的指针或引用(forward declaration)。例如:

class KeyShape; // forward declarationsize_t count_differences(KeyShape const& lhs, KeyShape const& rhs);

只要实现文件包含相应的头,就可以:

#include "key-shape.hpp" // KeyShape的完整定义size_t count_differences(KeyShape const& lhs, KeyShape const& rhs) {    assert(lhs.positions() == rhs.positions());    ...}

还可以将前向声明与某些模板化类一起使用,随模板参数不会改变大小,例如std::unique_ptr和std::vector。但是,这样做可能会需要你重新定义构造函数,析构函数和其他特殊成员函数(SMF),因为通常需要查看这些类型的完整定义。代码看起来像这样:

// foo.hpp#include class Bar;class Foo {    std::unique_ptr m_ptr;public:    Foo(); // = default;    ~Foo(); // = default;};
// foo.cpp#include "bar.hpp"Foo::Foo() = default;Foo::~Foo() = default;

这里仍然使用编译器生成的默认构造函数和析构函数,但是在.cpp文件中可以看到完整定义,但仍使用它Bar。这里习惯使用该// = default;注释来告诉其他程序员,已明确声明指定函数,但将使用默认实现,因此其中不会包含任何特殊逻辑。


显式概述

显式概述的基本思想很简单:如果将一段代码从一个函数中分离出来,通过内联减小函数的调用路径路径。而这样做的还有个好处是缩短编译时间。

抛出一个异常会生成大量代码,而引发更复杂的标准异常类型(例如std::runtime_error),也需要inclue重量级的头文件。

通过改为throw foo;使用辅助函数void throw_foo(char const* msg),调用开销变得更小,并且与该throw语句相关的所有编译成本都集中在单个模块中。即使对于仅存在于.cpp文件中的代码,这也是一个有用的优化。

简单的示例:如果没有更多的空间进行push_back,constexpr static_vector实现将引发std::logic_error。将比较两个版本:一个抛出异常的内联,和一个改为调用一个辅助函数。

内联抛出实现看起来像这样:

#include class static_vector {    int arr[10]{};    std::size_t idx = 0;public:    constexpr void push_back(int i) {        if (idx >= 10) {            throw std::logic_error("overflew static vector");        }        arr[idx++] = i;    }    constexpr std::size_t size() const { return idx; }};

另一个版本throw std::logic_error(...)行被调用throw_logic_errorhelper函数代替。做以下实验

#include "static-vector.hpp"void foo1(int n) {    static_vector vec;    for (int i = 0; i < n / 2; ++i) {        vec.push_back(i);    }}

在内联抛出异常情况下编译一个完整的二进制文件需要883.2 ms(±1.8),而在外联函数抛出下要花费285.5 ms (±0.8)。这是显著的(〜3倍)改进,并且随着包含static-vector.hpp标头的已编译目标文件数量的增加,改进也越发看到效果。当然,文件越复杂,改进就越小,因为报头的成本在总成本中所占的比例较小。


隐藏的友元

隐藏的友元是相关符号(函数/运算符)的可见性的模糊来减少重载集的大小。基本思想是只能通过参数依赖查找( Argument Dependent Lookup ,ADL)找到并调用在类内部声明的friend函数。然后,这意味着该函数将不参与重载解析,除非该表达式的“拥有”类型存在。

隐藏的友元操作符函数<<

struct A {    friend int operator<

在上面的代码段中,只有的第一个重载operator<<是隐藏的友元。第二次重载不是,因为它在A声明之外声明过。

减少过载集让编译速度更快,因为编译器要做的工作较少。


减少链接工作量

编译模型中,一个符号可能会出现在目标文件中,而其他目标文件无法使用。称这种符号具有内部链接。具有内部链接的符号在编译速度更快,是因为链接器不必随时跟踪它的可用状态,因此要做的工作较少。符号隐藏在内部还对运行时性能和目标文件大小有好处。

// local-linkage.cppstatic int helper1() { return -1; }namespace {    int helper2() { return  1; }}int do_stuff() { return helper1() + helper2(); }

在上面的示例中,helper1和helper2都是内部链接。helper1因为有static关键字,helper2因为它包含在一个未命名的名字空间中。我们可以用nm:

$ clang++ -c local-linkage.cpp && nm -C local-linkage.o0000000000000000 T do_stuff()0000000000000030 t helper1()0000000000000040 t (anonymous namespace)::helper2()

现在开启O1优化级别,helper1和helper2完全消失。这是因为它们足够小,可以内联do_stuff,并且来自别的的代码都无法引用它们,因为它们是内部链接。

$ clang++ -c local-linkage.cpp -O1 && nm -C local-linkage.o0000000000000000 T do_stuff()

这也是内部链接如何提高运行时性能的方式。因为编译器可以看到使用符号的所有位置,所以它可以将其内联到调用处,以完全删除该函数。

通过隐藏符号来提高编译性能通常很小。毕竟,链接每个符号所做的工作量很小。但是,大型二进制文件可以包含数百万个符号,就像与隐藏友元一样,隐藏符号也具有非编译性能优势,即可以防止在辅助函数之间违反ODR。


47a88301691deee335fba019ffc2584c.png
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。
<androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/round_style" android:elevation="4dp" android:layout_margin="8dp" android:clickable="true" android:onClick="onButtonClick" android:padding="10dp"> <com.xuexiang.xui.widget.textview.autofit.AutoFitTextView style="@style/item_show_title" android:id="@+id/spec" android:paddingTop="2dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"/> <com.xuexiang.xui.widget.textview.autofit.AutoFitTextView style="@style/item_show_title" android:id="@+id/style" app:layout_constraintStart_toEndOf="@+id/spec" app:layout_constraintTop_toTopOf="0"/> <com.xuexiang.xui.widget.textview.autofit.AutoFitTextView style="@style/item_show_title" android:id="@+id/name" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/spec"/> <com.xuexiang.xui.widget.textview.autofit.AutoFitTextView style="@style/item_show_title" android:id="@+id/real_inventory" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/name"/> <com.xuexiang.xui.widget.textview.autofit.AutoFitTextView style="@style/item_show_title" android:id="@+id/check_inventory" app:layout_constraintStart_toEndOf="@+id/real_inventory" app:layout_constraintTop_toBottomOf="@+id/name"/> <ImageView android:id="@+id/status" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@+id/name" app:layout_constraintTop_toTopOf="parent" /> <View style="@style/item_show" android:id="@+id/view_task_list" android:layout_width="match_parent" android:layout_height="1dp" android:background="#cccccc" android:layout_marginTop="3dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@+id/real_inventory" app:layout_constraintStart_toStartOf="@+id/real_inventory"/> </androidx.constraintlayout.widget.ConstraintLayout>调节间距
06-06

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值