你好,我是雨乐!
最近在升级系统和进行一些性能优化,业余时间也看一些技术书籍和视频,看了下上次更新文章的时间,大致在一个月前了,确实有点久了,所以赶紧拾起来,不能让大伙忘了我不是😁。
今天,聊聊在升级过程中的一个比较重要的优化点-编译期优化。
概述
说明符constexpr是自C++11引入,我相信很多人跟我一样,在第一次接触这个的时候,会很容易和const混淆。
从概念上理解的话,constexpr即常量表达式,重点在表达式字段,用于指定变量或函数可以在常量表达式中使用,可以(或者说一定)在编译时求值的表达式,而const则为了约束变量的访问控制,表示运行时不可以直接被修改,其往往可以在编译期和运行时进行初始化。
前面提到了constexpr是在编译阶段进行求值,那么也就是说在程序运行之前,就已经计算完成,这种无疑大大提升了程序的运行效率。因此提升运行效率就是C++11引入constexpr说明符的目的,也就是说能在编译阶段做的事情就绝不放在运行期做。
变量
代码如下:
example1.cc
int main() {
const int val = 1 + 2;
return 0;
}
上述代码汇编结果如下:
main:
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], 3
mov eax, 0
pop rbp
ret
从上述汇编结果可以看出,在编译阶段就将val赋值成3,也就是说在编译阶段完成了求值操作。
再看另外一个示例2:
example2.cc
int Add(const int a, const int b) {
return a + b;
}
int main() {
const int val = Add(1, 2);
return 0;
}
同样的,其汇编如下:
Add(int, int):
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov DWORD PTR [rbp-8], esi
mov edx, DWORD PTR [rbp-4]
mov eax, DWORD PTR [rbp-8]
add eax, edx
pop rbp
ret
main:
push rbp
mov rbp, rsp
sub rsp, 16
mov esi, 2
mov edi, 1
call Add(int, int)
mov DWORD PTR [rbp-4], eax
mov eax, 0
leave
ret
分析上述汇编,发现并没有在编译阶段进行求值,所以也就是说上述的求值过程将会延后至编译期进行。
好了,既然示例一(使用const)可以在编译期进行求值,而constexpr也可以在编译期求值,那么直接用constexpr替换示例一种的const是否可行?
example3.cc
int main() {
constexpr int val = 1 + 2;
return 0;
}
接着看下汇编代码:
main:
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], 3
mov eax, 0
pop rbp
ret
呃😓,与示例一完全一样。。。
在上面示例2中,通过汇编代码发现其是在运行期求值,那么有没有办法在编译期求值呢?那就是使用constexpr表达式:
example4.cc
constexpr int Add(const int a, const int b) {
return a + b;
}
int main() {
const int val = Add(1, 2);
return 0;
}
汇编如下:
main:
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], 3
mov eax, 0
pop rbp
ret
有没有发现很眼熟,对,跟示例1和示例3的结果一样,该代码较示例2的唯一区别是多了个constexpr说明符,但将求值时期从运行期放到了编译期,可想而知,效率提升那是杠杠的。。。😁
函数
constexpr也可以修饰普通函数或者成员函数,其实这块在上一节已经有提过,示例如下:
constexpr int Add(const int a, const int b) {
return a + b;
}
int main() {
const int val = Add(1, 2);
int val1 = 3;
int val2 = Add(val, val1);
return 0;
}
汇编如下:
Add(int, int):
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov DWORD PTR [rbp-8], esi
mov edx, DWORD PTR [rbp-4]
mov eax, DWORD PTR [rbp-8]
add eax, edx
pop rbp
ret
main:
push rbp
mov rbp, rsp
sub rsp, 16
mov DWORD PTR [rbp-4], 3
mov DWORD PTR [rbp-8], 3
mov eax, DWORD PTR [rbp-8]
mov esi, eax
mov edi, 3
call Add(int, int)
mov DWORD PTR [rbp-12], eax
mov eax, 0
leave
ret
从上述汇编代码可以看出,val的求值是在编译阶段,而val2的求值则是在运行阶段,这是因为其引入了一个非const变量val1。
通过本示例,可以看出,将函数声明为constexpr可以提示效率,让编译器来决定是在编译阶段还是运行阶段来进行求值,当然了,如果想了解在编译阶段求值的各种细节规则,请参考constexpr in cppreference。
if语句
如果您目前使用C++11进行编码,那么需要仔细阅读本节,这样可以为将来的版本升级打好基础;如果您正在使用C++17进行编码,那么更得阅读本节,相信读完本节后,会有一个不一样的认识😁。
自C++17起,引入了if constexpr语句,在本节中,将借助SFINAE 和 std::enable_if来实现一个简单的Square功能,最后借助if constexpr对代码进行优化(如果对SFINAE 和 std::enable_if不是很了解的,建议自行阅读哈)。
如果有个需求,实现一个Add函数,其既支持算术类型又支持用户自定义类型:
template <typename T>
struct Number {
Number(const T& _val) :
value(_val) {}
T value;
};
template<typename T>
T Square(const T& t) {
return t + t;
}
int main() {
int i = 5;
float f = 5.0;
bool b = true;
Number<int> n(5);
auto res = Square(i); // 调用int Add(int);
auto res2 = Square(f); // 调用 float Add(float);
auto res3 = Square(b); // call bool Square(bool);
auto res4 = Square(n); //编译失败,因为Number<>没有提供operator*操作
}
上述代码编译出错,因为Number<>没有提供operator*操作,所以这个时候第一个想法是修改Square函数,如下:
template<typename T>
T Square(const T& t) {
if (std::is_arithmetic<T>::value) {
return t * t;
} else {
return t.value * t.value;
}
}
在上述代码中,如果T是算数类型,则直接进行*操作,否则取其value进行*操作。
将上述代码进行编译,报错如下:
example5.cc: In instantiation of ‘T Square(const T&) [with T = int]’:
example5.cc:26:20: required from here
example5.cc:16:18: error: request for member ‘value’ in ‘t’, which is of non-class type ‘const int’
16 | return t.value * t.value;
| ~~^~~~~
example5.cc:16:28: error: request for member ‘value’ in ‘t’, which is of non-class type ‘const int’
16 | return t.value * t.value;
| ~~^~~~~
....
以Square(i)为例,这是因为在编译的时候,会尝试int.value操作,显然int.value不存在,这就导致了上述的错误输出,为了更为清楚的显示本错误,将Square()修改如下:
int Square(const int& t) {
if (true) {
return t * t;
} else {
return t.value * t.value;
}
}
这样就能很清楚的知道为什么编译失败了,因为在代码中存在t.value * t.value操作,而对于一个int来说并没有value这个变量,所以编译失败。
为了解决这个问题,我们尝试引入std::enable_if操作,如下:
template<typename T>
typename std::enable_if<std::is_arithmetic<T>::value, T>::type Square(const T& t) {
return t * t;
}
template<typename T>
typename std::enable_if<! std::is_arithmetic<T>::value, T>::type Square(const T& t) {
return t.value * t.value;
}
现在有两个函数模板,如果是算术类型,则调用第一个,否则调用第二个,完整代码如下:
#include <type_traits>
template<typename T>
typename std::enable_if<std::is_arithmetic<T>::value, T>::type Square(const T& t) {
return t * t;
}
template<typename T>
typename std::enable_if<! std::is_arithmetic<T>::value, T>::type Square(const T& t) {
return t.value * t.value;
}
template <typename T>
struct Number {
Number(const T& _val) :
value(_val) {}
T value;
};
int main() {
int i = 5;
float f = 5.0;
bool b = true;
Number<int> n(5);
auto res = Square(i); // 调用int Add(int);
auto res2 = Square(f); // 调用 float Add(float);
auto res3 = Square(b); // call bool Square(bool);
auto res4 = Square(n); // 成功
return 0;
}
上述代码编译成功。
在上述代码中,为了编译成功,我们引入了两个Square()模板函数借助std::enable_if来实现,代码上多少有点冗余,在这个时候,本节的主角if constexpr 出场,完整代码如下:
#include <type_traits>
template<typename T>
T Square(const T& t) {
if constexpr (std::is_arithmetic<T>::value) {
return t * t;
} else {
return t.value * t.value;
}
}
template <typename T>
struct Number {
Number(const T& _val) :
value(_val) {}
T value;
};
int main() {
int i = 5;
float f = 5.0;
bool b = true;
Number<int> n(5);
auto res = Square(i); // 调用int Add(int);
auto res2 = Square(f); // 调用 float Add(float);
auto res3 = Square(b); // call bool Square(bool);
auto res4 = Square(n); // 成功
return 0;
}
编译成功。
我们借助一个Square()函数模板以及更加符合编码习惯的if语句就能解决上面的问题,且比使用std::enable_if方式更为优雅和符合阅读习惯,进而提高代码的可阅读性。