C++性能优化笔记-14-元编程


元编程意味着开发可以生成代码的代码。例如,在解释型脚本语言中,通常可以开发一段生成字符串的代码,然后将此字符串解释为代码。

元编程在编译型语言如C++中有帮助。如果计算所需的所有输入在编译时可知,则可以在编译时而不是在运行时进行计算。当然,在解释型语言中没有这样的优势,因为一切都在运行时发生。

以下技术可以视为C++中的元编程:

  • 预处理器指令。例如,使用#if而不是if。这是一个非常有用的删除多余代码的方法,但有严重的限制,预处理器位于编译器之前,并且它只理解最简单的表达式和运算符。
  • 开发一个C++程序,它生成另一个C++程序(或它的一部分)。某些情况下很有用,例如,生成一个你需要的数学函数的列表,最终程序中当做常量数组使用。当然,这需要你编译第一个程序的输出。
  • 优化编译器可能会在编译时做尽可能多的处理。例如,所有编译器都会把int x=2*5;替换为int x=10;
  • 模板在编译时实例化。模板实例其参数被编译前的实际值替换。这就是为什么使用模板几乎没有成本。从理论上讲,几乎所有的算法都可以使用模板元编程,但这种方法非常简单复杂而笨拙,编译可能需要很长时间。
  • 编译时分支。if constexpr (boolean expression) {}。这个括号内的布尔表达式可以是任何在编译时已知值的表达式。如果布尔值为真,{}中的代码将才包含在最终版本中。否则,{}中的代码会删除,但语法需要正确。如果语法检查发生在模板内部,那么它就不那么严格了。此功能对于决定使用哪个版本的代码非常有用。if constexpr特性对于条件声明不太有用,因为该声明仅限于{}。编译时分支要求编译器支持C++17标准。
  • constexpr函数。constexpr函数可以在编译时执行任意计算,只要在编译时使用已知的参数调用它。需要编译器支持C++14标准或更高版本。

模板元编程

以下示例显示了当指数是编译时已知的整数时,如何使用元编程加速幂函数的计算。
。。。
正如我们所看到的,仅用四次乘法就可以计算pow (x,10)。如何从示例15.1b到15.1c?我们利用了一个事实,即n是编译时已知的,以消除所有仅依赖于n的内容,包括while循环、if语句和所有整数计算。示例15.1c中的代码比15.1b快,在这种情况下,代码也可能更小。
从示例15.1b到15.1c的转换是由我手动完成的,但是如果我们想要生成一段适用于任何编译时常量n的代码,那么我们需要元编程。只有最好的编译器才会自动将示例15.1a或15.1b转换为15.1c。元编程对于编译器无法自动优化的情况非常有用。
下一个示例显示了使用模板元编程的实现。模板元编程可能非常复杂。幸运的是,新的C++17标准提供了一种更简单的方法,我们将在下一章中看到。

我给出这个例子只是为了说明元编程可以是多么的曲折和复杂。
。。。
在C++模板元编程中,循环被实现为递归。powN模板正在调用自身以模拟示例15.1b中的while循环。分支由(部分)模板专门实现。这就是示例15.1b中的if分支的实现方式。递归必须始终以非递归模板结束,而不是以模板内部的分支结束。
powN模板是类模板而不是函数模板,因为部分模板特定实现只允许用于类。将N分解为二进制表示的各个位尤其棘手。我使用了一个技巧N1=N &(N-1)给出了N去掉了最右边的1位的值。如果N是2的幂,则N & (N-1)为0。常数N1也可以不用宏,而用其他方式定义,但这里使用的方法是我尝试过的所有编译器上唯一都有效的方法。
好的编译器实际上按照预期将示例15.1d缩减为15.1c,因为它们可以消除常见的子表达式。

为什么模板元编程如此复杂?因为C++模板功能不是为此而设计的。只是碰巧可能的。模板元编程非常复杂,我认为除非在最简单的情况下,否则使用它是不明智的。复杂的代码本身就是一个风险因素,并且验证、调试和维护此类代码的成本非常高,因此与获得性能提升相比很少是值得的。

编译期常量分支元编程

幸运的是,C++17标准使元编程变得更简单,它提供了带有constexpr关键字的编译时分支。下面的示例显示了与示例15.1相同的算法,使用编译时分支。
。。。
在这里,我们仍然需要一个递归模板来展开循环,但是创建分支和结束递归更容易。一个好的编译器会将示例15.2简化为乘法指令而不是其他指令。未执行的分支将不包括在最终代码中。
在C++17引入编译时分支之前,我们遇到了一个问题,即未执行分支中的模板会被展开,即使从未使用过。这可能导致无限递归或未使用分支的数量呈指数级增长。在C++17之前,结束模板递归的唯一方法是使用模板特殊的实现,如示例15.1d所示。如果递归模板包含许多分支,则编译它可能需要很长时间。

编译期常量函数元编程

constexpr函数是一个函数,如果参数是编译时常量,它可以在编译时执行几乎任何计算。使用C++14标准,可以在constexpr函数中拥有分支、循环等。

此示例查找整数中最高有效位的位置。这与位反向扫描指令相同,但在编译时进行计算:
。。。
一个好的优化编译器都会在编译时进行简单的计算,如果所有的输入都是已知的常数。但是很少有编译器能够在编译时进行更复杂的计算,如果它们涉及分支、循环、函数调用。constexpr函数可用于确保在编译时完成某些计算。
constexpr函数可用于任何需要编译时常量的地方,例如数组大小或编译时分支。

虽然C++ 14和C++ 17提供了重要改进,提高了元编程的可能性。但仍然有一些事情是不能用C++语言来完成的。例如,不可能使编译时循环10次来生成名为func1、func2、…、func10的十个函数。这在某些脚本语言和汇编程序中是可能的。

欢迎交流
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值