是否有技术可以大大缩短 3D 应用程序的 C++ 构建时间?
Are there techniques to greatly improve C++ building time for 3D applications?
有很多超薄笔记本电脑,它们既便宜又好用。编程的优点是可以在安静舒适的任何地方进行,因为长时间集中精力是能够有效工作的重要因素。
我有点过时,因为我喜欢静态编译的 C 或 C++,这些语言在那些功率受限的笔记本电脑上编译可能会很长,尤其是 C++11 和 C++14。
我喜欢做 3D 编程,我使用的库可能很大而且不会宽容:bullet physics、Ogre3D、SFML,更不用说现代 IDEs 的电力需求了。
有几种解决方案可以加快构建速度:
解决方案A:不要使用那些大库,自己想出一些更轻的东西来减轻编译器的负担。编写适当的 makefile,不要使用 IDE.
解决方案B:在别处设置构建服务器,在功能强大的机器上设置makefile,并自动下载生成的exe。我不认为这是一个随意的解决方案,因为您必须针对笔记本电脑的 CPU.
方案C:使用非官方C++模块
???
还有其他建议吗?
正如 Yellow 先生在评论中指出的那样,缩短编译时间的最佳方法之一是特别注意您对 header 文件的使用。特别是:
- 对您不希望更改的任何 header 使用预编译 header,包括操作系统 header、第三方库 header 等。
- 将其他 header 中包含的 header 的数量减少到必要的最低限度。
- 确定 header 中是否需要包含或是否可以将其移动到 cpp 文件中。这有时会引起连锁反应,因为其他人依赖于您为它包含 header,但从长远来看,最好将包含移动到实际需要的地方。
- 使用前向声明的 classes 等通常可以消除包含声明 class 的 header 的需要。当然,您仍然需要在 cpp 文件中包含 header,但这只会发生一次,而不是每次包含相应的 header 文件时都会发生。
- 使用#pragma once(如果您的编译器支持)而不是包括保护符号。这意味着编译器甚至不需要打开 header 文件来发现包含防护。 (当然,许多现代编译器无论如何都会为您解决这个问题。)
一旦您控制了 header 文件,请检查您的 make 文件以确保您不再有不必要的依赖项。目标是重建您需要的一切,但仅此而已。有时人们宁愿建得太多也会犯错,因为这比建得太少更安全。
编译速度确实可以提高,如果你知道怎么做的话。仔细考虑项目的设计(特别是在大型项目的情况下,由多个模块组成)并修改它总是明智的,这样编译器才能有效地产生输出。
1.预编译 headers.
预编译 header 是一个普通的 header(.h
文件),其中包含最常见的声明、typedef 和 include。在编译期间,它只被解析一次——在编译任何其他源之前。在此过程中,编译器生成一些内部(最有可能是二进制)格式的数据,然后,它使用这些数据来加速代码生成。
这是一个示例:
#pragma once
#ifndef __Asx_Core_Prerequisites_H__
#define __Asx_Core_Prerequisites_H__
//Include common headers
#include "BaseConfig.h"
#include "Atomic.h"
#include "Limits.h"
#include "DebugDefs.h"
#include "CommonApi.h"
#include "Algorithms.h"
#include "HashCode.h"
#include "MemoryOverride.h"
#include "Result.h"
#include "ThreadBase.h"
//Others...
namespace Asx
{
//Forward declare common types
class String;
class UnicodeString;
//Declare global constants
enum : Enum
{
ID_Auto = Limits<Enum>::Max_Value,
ID_None = 0
};
enum : Size_t
{
Max_Size = Limits<Size_t>::Max_Value,
Invalid_Position = Limits<Size_t>::Max_Value
};
enum : Uint
{
Timeout_Infinite = Limits<Uint>::Max_Value
};
//Other things...
}
#endif /* __Asx_Core_Prerequisites_H__ */
在项目中,当使用 PCH 时,每个源文件通常包含 #include
这个文件(我不知道其他人,但在 VC++ 这实际上是一个要求 - 每个源附加到配置为使用 PCH 的项目,必须以:#include PrecompiledHedareName.h
) 开头。预编译 headers 的配置非常 platform-dependent 并且超出了这个答案的范围。
注意一件重要的事情:PCH 中 defined/included 的东西只有在绝对必要时才应该更改 - 每次更改都可能导致重新编译 整个项目 (以及其他依赖模块)!
关于 PCH 的更多信息:
2。转发声明。
当您不需要整个 class 定义时,转发声明它以删除代码中不必要的依赖项。这也意味着在可能的情况下广泛使用指针和引用。示例:
#include "BigDataType.h"
class Sample
{
protected:
BigDataType _data;
};
您真的需要将 _data
存储为值吗?为什么不这样:
class BigDataType; //That's enough, #include not required
class Sample
{
protected:
BigDataType* _data; //So much better now
};
这对于大型类型来说尤其有利可图。
3。不要过度使用模板。
Meta-programming 是开发人员工具箱中非常强大的工具。但是不要在没有必要的时候尝试使用它们。
它们非常适用于特征、compile-time 评估、静态反射等。但是他们引入了很多麻烦:
- 错误消息 - 如果您曾经看到因不当使用
std::
迭代器或容器(尤其是复杂的,如 std::unordered_map
)而导致的错误,那么您就会知道这是怎么回事。
- 可读性 - 复杂的模板可能很难 read/modify/maintain。
- 怪癖 - 许多技术,模板用于,不是这样 well-known,因此维护此类代码可能会更加困难。
- 编译时间——现在对我们来说最重要的:
记住,如果您将函数定义为:
template <class Tx, class Ty>
void sample(const Tx& xv, const Ty& yv)
{
//body
}
它将针对Tx
和Ty
的每个独占组合进行编译。如果经常使用这样的函数(以及许多这样的组合),它确实会减慢编译过程。现在想象一下,如果您开始为整个 classes 过度使用模板,将会发生什么……
4.使用 PIMPL idiom.
这是一项非常有用的技术,它使我们能够:
- 隐藏实施细节
- 加快代码生成速度
- 轻松更新,无需破坏客户端代码
它是如何工作的?考虑 class,其中包含大量数据(例如,代表人)。它可能看起来像这样:
class Person
{
protected:
string name;
string surname;
Date birth_date;
Date registration_date;
string email_address;
//and so on...
};
我们的应用程序不断发展,我们需要extend/change Person
定义。我们添加了一些新字段,删除了其他字段……一切都崩溃了:Person 的大小发生变化,字段名称发生变化……灾难。特别是,每个依赖于 Person
定义的客户端代码都需要是 changed/updated/fixed。不好。
但我们可以用更聪明的方式来做到这一点——隐藏 Person 的详细信息:
class Person
{
protected:
class Details;
Details* details;
};
现在,我们做了一些好事:
- 客户端无法创建代码,这取决于
Person
的定义方式
- 只要我们不修改客户端代码使用的public接口,就不需要重新编译
- 我们减少了编译时间,因为
string
和 Date
的定义不再需要存在(在以前的版本中,我们必须为这些类型包含适当的 headers ,这会增加额外的依赖关系)。
虽it may give no speed boost,但更清晰,少error-prone。它与使用 include guards 基本相同:
#ifndef __Asx_Core_Prerequisites_H__
#define __Asx_Core_Prerequisites_H__
//Content
#endif /* __Asx_Core_Prerequisites_H__ */
防止对同一个文件进行多次解析。尽管 #pragma once
不是标准的(事实上,没有 pragma - pragma 是为 compiler-specific 指令保留的),但它得到了广泛的支持(例如:VC++、GCC、CLang、ICC ) 并且可以毫无顾虑地使用——编译器应该忽略未知的编译指示(或多或少地默默地)。
6.消除不必要的依赖。
非常重要的一点!重构代码时,依赖项通常会发生变化。例如,如果您决定做某事优化并使用 pointers/references 而不是值(参见此答案的点 2 和 4),一些包含可能变得不必要。考虑:
#include "Time.h"
#include "Day.h"
#include "Month.h"
#include "Timezone.h"
class Date
{
protected:
Time time;
Day day;
Month month;
Uint16 year;
Timezone tz;
//...
};
此 class 已更改为隐藏实现细节:
//These are no longer required!
//#include "Time.h"
//#include "Day.h"
//#include "Month.h"
//#include "Timezone.h"
class Date
{
protected:
class Details;
Details* details;
//...
};
最好跟踪此类冗余包含,使用大脑,built-in 工具(如 VS Dependency Visualizer) or external utilities (for example, GraphViz)。
Visual Studio 还有一个很好的选项——如果你用 RMB 点击任何文件,你会看到一个选项 'Generate Graph of include files'——它会生成一个漂亮的、可读的图表,可以很容易地分析并用于跟踪不必要的依赖关系。
示例图,在我的 String.h
文件中生成:
还有一点在其他答案中没有提到:模板。模板可以是一个很好的工具,但它们有根本的缺点:
必须包含模板及其依赖的所有模板。前向声明不起作用。
模板代码经常被多次编译。您在多少个 .cpp 文件中使用 std::vector<>
?那就是你的编译器需要编译多少次!
(我不是反对使用std::vector<>
,相反你应该经常使用它;它只是一个非常常用的模板的例子在这里。)
更改模板的实现时,必须重新编译使用该模板的所有内容。
对于模板繁重的代码,编译单元通常相对较少,但每个编译单元都很大。当然,您可以使用所有模板,并且只有一个 .cpp 文件可以包含所有内容。这将避免模板代码的多次编译,但是它使 make
无用:任何编译都将花费与干净后的编译一样长的时间。
我建议反其道而行之:避免使用大量模板或仅使用模板的库,并避免创建复杂的模板。您的模板变得越相互依赖,重复编译的次数就越多,更改模板时需要重建的 .cpp 文件就越多。理想情况下,您拥有的任何模板都不应使用任何其他模板(当然,除非其他模板是 std::vector<>
...)。
如果您已经尝试了上述所有方法,假设您的 LAN 上有一些可用的 PC,那么有一种商业产品可以创造奇迹。我们曾经在以前的工作中使用过它。它称为 Incredibuild (www.incredibuild.com),它将我们的构建时间从一个多小时 (C++) 缩短到大约 10 分钟。来自他们的网站:
IncrediBuild 通过高效的并行计算加快构建时间。通过利用网络上的闲置 CPU 资源,IncrediBuild 将 PC 和服务器网络转变为私有计算云,最好将其描述为“虚拟超级计算机”。进程分布到远程 CPU 资源以进行并行处理,显着缩短构建时间达 90% 或更多。
有很多超薄笔记本电脑,它们既便宜又好用。编程的优点是可以在安静舒适的任何地方进行,因为长时间集中精力是能够有效工作的重要因素。
我有点过时,因为我喜欢静态编译的 C 或 C++,这些语言在那些功率受限的笔记本电脑上编译可能会很长,尤其是 C++11 和 C++14。
我喜欢做 3D 编程,我使用的库可能很大而且不会宽容:bullet physics、Ogre3D、SFML,更不用说现代 IDEs 的电力需求了。
有几种解决方案可以加快构建速度:
解决方案A:不要使用那些大库,自己想出一些更轻的东西来减轻编译器的负担。编写适当的 makefile,不要使用 IDE.
解决方案B:在别处设置构建服务器,在功能强大的机器上设置makefile,并自动下载生成的exe。我不认为这是一个随意的解决方案,因为您必须针对笔记本电脑的 CPU.
方案C:使用非官方C++模块
???
还有其他建议吗?
正如 Yellow 先生在评论中指出的那样,缩短编译时间的最佳方法之一是特别注意您对 header 文件的使用。特别是:
- 对您不希望更改的任何 header 使用预编译 header,包括操作系统 header、第三方库 header 等。
- 将其他 header 中包含的 header 的数量减少到必要的最低限度。
- 确定 header 中是否需要包含或是否可以将其移动到 cpp 文件中。这有时会引起连锁反应,因为其他人依赖于您为它包含 header,但从长远来看,最好将包含移动到实际需要的地方。
- 使用前向声明的 classes 等通常可以消除包含声明 class 的 header 的需要。当然,您仍然需要在 cpp 文件中包含 header,但这只会发生一次,而不是每次包含相应的 header 文件时都会发生。
- 使用#pragma once(如果您的编译器支持)而不是包括保护符号。这意味着编译器甚至不需要打开 header 文件来发现包含防护。 (当然,许多现代编译器无论如何都会为您解决这个问题。)
一旦您控制了 header 文件,请检查您的 make 文件以确保您不再有不必要的依赖项。目标是重建您需要的一切,但仅此而已。有时人们宁愿建得太多也会犯错,因为这比建得太少更安全。
编译速度确实可以提高,如果你知道怎么做的话。仔细考虑项目的设计(特别是在大型项目的情况下,由多个模块组成)并修改它总是明智的,这样编译器才能有效地产生输出。
1.预编译 headers.
预编译 header 是一个普通的 header(.h
文件),其中包含最常见的声明、typedef 和 include。在编译期间,它只被解析一次——在编译任何其他源之前。在此过程中,编译器生成一些内部(最有可能是二进制)格式的数据,然后,它使用这些数据来加速代码生成。
这是一个示例:
#pragma once
#ifndef __Asx_Core_Prerequisites_H__
#define __Asx_Core_Prerequisites_H__
//Include common headers
#include "BaseConfig.h"
#include "Atomic.h"
#include "Limits.h"
#include "DebugDefs.h"
#include "CommonApi.h"
#include "Algorithms.h"
#include "HashCode.h"
#include "MemoryOverride.h"
#include "Result.h"
#include "ThreadBase.h"
//Others...
namespace Asx
{
//Forward declare common types
class String;
class UnicodeString;
//Declare global constants
enum : Enum
{
ID_Auto = Limits<Enum>::Max_Value,
ID_None = 0
};
enum : Size_t
{
Max_Size = Limits<Size_t>::Max_Value,
Invalid_Position = Limits<Size_t>::Max_Value
};
enum : Uint
{
Timeout_Infinite = Limits<Uint>::Max_Value
};
//Other things...
}
#endif /* __Asx_Core_Prerequisites_H__ */
在项目中,当使用 PCH 时,每个源文件通常包含 #include
这个文件(我不知道其他人,但在 VC++ 这实际上是一个要求 - 每个源附加到配置为使用 PCH 的项目,必须以:#include PrecompiledHedareName.h
) 开头。预编译 headers 的配置非常 platform-dependent 并且超出了这个答案的范围。
注意一件重要的事情:PCH 中 defined/included 的东西只有在绝对必要时才应该更改 - 每次更改都可能导致重新编译 整个项目 (以及其他依赖模块)!
关于 PCH 的更多信息:
2。转发声明。
当您不需要整个 class 定义时,转发声明它以删除代码中不必要的依赖项。这也意味着在可能的情况下广泛使用指针和引用。示例:
#include "BigDataType.h"
class Sample
{
protected:
BigDataType _data;
};
您真的需要将 _data
存储为值吗?为什么不这样:
class BigDataType; //That's enough, #include not required
class Sample
{
protected:
BigDataType* _data; //So much better now
};
这对于大型类型来说尤其有利可图。
3。不要过度使用模板。
Meta-programming 是开发人员工具箱中非常强大的工具。但是不要在没有必要的时候尝试使用它们。
它们非常适用于特征、compile-time 评估、静态反射等。但是他们引入了很多麻烦:
- 错误消息 - 如果您曾经看到因不当使用
std::
迭代器或容器(尤其是复杂的,如std::unordered_map
)而导致的错误,那么您就会知道这是怎么回事。 - 可读性 - 复杂的模板可能很难 read/modify/maintain。
- 怪癖 - 许多技术,模板用于,不是这样 well-known,因此维护此类代码可能会更加困难。
- 编译时间——现在对我们来说最重要的:
记住,如果您将函数定义为:
template <class Tx, class Ty>
void sample(const Tx& xv, const Ty& yv)
{
//body
}
它将针对Tx
和Ty
的每个独占组合进行编译。如果经常使用这样的函数(以及许多这样的组合),它确实会减慢编译过程。现在想象一下,如果您开始为整个 classes 过度使用模板,将会发生什么……
4.使用 PIMPL idiom.
这是一项非常有用的技术,它使我们能够:
- 隐藏实施细节
- 加快代码生成速度
- 轻松更新,无需破坏客户端代码
它是如何工作的?考虑 class,其中包含大量数据(例如,代表人)。它可能看起来像这样:
class Person
{
protected:
string name;
string surname;
Date birth_date;
Date registration_date;
string email_address;
//and so on...
};
我们的应用程序不断发展,我们需要extend/change Person
定义。我们添加了一些新字段,删除了其他字段……一切都崩溃了:Person 的大小发生变化,字段名称发生变化……灾难。特别是,每个依赖于 Person
定义的客户端代码都需要是 changed/updated/fixed。不好。
但我们可以用更聪明的方式来做到这一点——隐藏 Person 的详细信息:
class Person
{
protected:
class Details;
Details* details;
};
现在,我们做了一些好事:
- 客户端无法创建代码,这取决于
Person
的定义方式 - 只要我们不修改客户端代码使用的public接口,就不需要重新编译
- 我们减少了编译时间,因为
string
和Date
的定义不再需要存在(在以前的版本中,我们必须为这些类型包含适当的 headers ,这会增加额外的依赖关系)。
虽it may give no speed boost,但更清晰,少error-prone。它与使用 include guards 基本相同:
#ifndef __Asx_Core_Prerequisites_H__
#define __Asx_Core_Prerequisites_H__
//Content
#endif /* __Asx_Core_Prerequisites_H__ */
防止对同一个文件进行多次解析。尽管 #pragma once
不是标准的(事实上,没有 pragma - pragma 是为 compiler-specific 指令保留的),但它得到了广泛的支持(例如:VC++、GCC、CLang、ICC ) 并且可以毫无顾虑地使用——编译器应该忽略未知的编译指示(或多或少地默默地)。
6.消除不必要的依赖。
非常重要的一点!重构代码时,依赖项通常会发生变化。例如,如果您决定做某事优化并使用 pointers/references 而不是值(参见此答案的点 2 和 4),一些包含可能变得不必要。考虑:
#include "Time.h"
#include "Day.h"
#include "Month.h"
#include "Timezone.h"
class Date
{
protected:
Time time;
Day day;
Month month;
Uint16 year;
Timezone tz;
//...
};
此 class 已更改为隐藏实现细节:
//These are no longer required!
//#include "Time.h"
//#include "Day.h"
//#include "Month.h"
//#include "Timezone.h"
class Date
{
protected:
class Details;
Details* details;
//...
};
最好跟踪此类冗余包含,使用大脑,built-in 工具(如 VS Dependency Visualizer) or external utilities (for example, GraphViz)。
Visual Studio 还有一个很好的选项——如果你用 RMB 点击任何文件,你会看到一个选项 'Generate Graph of include files'——它会生成一个漂亮的、可读的图表,可以很容易地分析并用于跟踪不必要的依赖关系。
示例图,在我的 String.h
文件中生成:
还有一点在其他答案中没有提到:模板。模板可以是一个很好的工具,但它们有根本的缺点:
必须包含模板及其依赖的所有模板。前向声明不起作用。
模板代码经常被多次编译。您在多少个 .cpp 文件中使用
std::vector<>
?那就是你的编译器需要编译多少次!(我不是反对使用
std::vector<>
,相反你应该经常使用它;它只是一个非常常用的模板的例子在这里。)更改模板的实现时,必须重新编译使用该模板的所有内容。
对于模板繁重的代码,编译单元通常相对较少,但每个编译单元都很大。当然,您可以使用所有模板,并且只有一个 .cpp 文件可以包含所有内容。这将避免模板代码的多次编译,但是它使 make
无用:任何编译都将花费与干净后的编译一样长的时间。
我建议反其道而行之:避免使用大量模板或仅使用模板的库,并避免创建复杂的模板。您的模板变得越相互依赖,重复编译的次数就越多,更改模板时需要重建的 .cpp 文件就越多。理想情况下,您拥有的任何模板都不应使用任何其他模板(当然,除非其他模板是 std::vector<>
...)。
如果您已经尝试了上述所有方法,假设您的 LAN 上有一些可用的 PC,那么有一种商业产品可以创造奇迹。我们曾经在以前的工作中使用过它。它称为 Incredibuild (www.incredibuild.com),它将我们的构建时间从一个多小时 (C++) 缩短到大约 10 分钟。来自他们的网站:
IncrediBuild 通过高效的并行计算加快构建时间。通过利用网络上的闲置 CPU 资源,IncrediBuild 将 PC 和服务器网络转变为私有计算云,最好将其描述为“虚拟超级计算机”。进程分布到远程 CPU 资源以进行并行处理,显着缩短构建时间达 90% 或更多。