性能優(yōu)化利器之Constexpr
你好,我是雨樂!
最近在升級系統(tǒng)和進(jìn)行一些性能優(yōu)化,業(yè)余時間也看一些技術(shù)書籍和視頻,看了下上次更新文章的時間,大致在一個月前了,確實有點(diǎn)久了,所以趕緊拾起來,不能讓大伙忘了我不是??。
今天,聊聊在升級過程中的一個比較重要的優(yōu)化點(diǎn)-編譯期優(yōu)化。
概述
說明符constexpr是自C++11引入,我相信很多人跟我一樣,在第一次接觸這個的時候,會很容易和const混淆。
從概念上理解的話,constexpr即常量表達(dá)式,重點(diǎn)在表達(dá)式字段,用于指定變量或函數(shù)可以在常量表達(dá)式中使用,可以(或者說一定)在編譯時求值的表達(dá)式,而const則為了約束變量的訪問控制,表示運(yùn)行時不可以直接被修改,其往往可以在編譯期和運(yùn)行時進(jìn)行初始化。
前面提到了constexpr是在編譯階段進(jìn)行求值,那么也就是說在程序運(yùn)行之前,就已經(jīng)計算完成,這種無疑大大提升了程序的運(yùn)行效率。因此提升運(yùn)行效率就是C++11引入constexpr說明符的目的,也就是說能在編譯階段做的事情就絕不放在運(yùn)行期做。
變量
代碼如下:
example1.cc
int main() {
const int val = 1 + 2;
return 0;
}
上述代碼匯編結(jié)果如下:
main:
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], 3
mov eax, 0
pop rbp
ret
從上述匯編結(jié)果可以看出,在編譯階段就將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
分析上述匯編,發(fā)現(xiàn)并沒有在編譯階段進(jìn)行求值,所以也就是說上述的求值過程將會延后至編譯期進(jìn)行。
好了,既然示例一(使用const)可以在編譯期進(jìn)行求值,而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中,通過匯編代碼發(fā)現(xiàn)其是在運(yùn)行期求值,那么有沒有辦法在編譯期求值呢?那就是使用constexpr表達(dá)式:
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
有沒有發(fā)現(xiàn)很眼熟,對,跟示例1和示例3的結(jié)果一樣,該代碼較示例2的唯一區(qū)別是多了個constexpr說明符,但將求值時期從運(yùn)行期放到了編譯期,可想而知,效率提升那是杠杠的。。。??
函數(shù)
constexpr也可以修飾普通函數(shù)或者成員函數(shù),其實這塊在上一節(jié)已經(jīng)有提過,示例如下:
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的求值則是在運(yùn)行階段,這是因為其引入了一個非const變量val1。
通過本示例,可以看出,將函數(shù)聲明為constexpr可以提示效率,讓編譯器來決定是在編譯階段還是運(yùn)行階段來進(jìn)行求值,當(dāng)然了,如果想了解在編譯階段求值的各種細(xì)節(jié)規(guī)則,請參考constexpr in cppreference。
if語句
如果您目前使用C++11進(jìn)行編碼,那么需要仔細(xì)閱讀本節(jié),這樣可以為將來的版本升級打好基礎(chǔ);如果您正在使用C++17進(jìn)行編碼,那么更得閱讀本節(jié),相信讀完本節(jié)后,會有一個不一樣的認(rèn)識??。
自C++17起,引入了if constexpr語句,在本節(jié)中,將借助SFINAE 和 std::enable_if來實現(xiàn)一個簡單的Square功能,最后借助if constexpr對代碼進(jìn)行優(yōu)化(如果對SFINAE 和 std::enable_if不是很了解的,建議自行閱讀哈)。
如果有個需求,實現(xiàn)一個Add函數(shù),其既支持算術(shù)類型又支持用戶自定義類型:
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); // 調(diào)用int Add(int);
auto res2 = Square(f); // 調(diào)用 float Add(float);
auto res3 = Square(b); // call bool Square(bool);
auto res4 = Square(n); //編譯失敗,因為Number<>沒有提供operator*操作
}
上述代碼編譯出錯,因為Number<>沒有提供operator*操作,所以這個時候第一個想法是修改Square函數(shù),如下:
template<typename T>
T Square(const T& t) {
if (std::is_arithmetic<T>::value) {
return t * t;
} else {
return t.value * t.value;
}
}
在上述代碼中,如果T是算數(shù)類型,則直接進(jìn)行*操作,否則取其value進(jìn)行*操作。
將上述代碼進(jìn)行編譯,報錯如下:
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不存在,這就導(dǎo)致了上述的錯誤輸出,為了更為清楚的顯示本錯誤,將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;
}
現(xiàn)在有兩個函數(shù)模板,如果是算術(shù)類型,則調(diào)用第一個,否則調(diào)用第二個,完整代碼如下:
#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); // 調(diào)用int Add(int);
auto res2 = Square(f); // 調(diào)用 float Add(float);
auto res3 = Square(b); // call bool Square(bool);
auto res4 = Square(n); // 成功
return 0;
}
上述代碼編譯成功。
在上述代碼中,為了編譯成功,我們引入了兩個Square()模板函數(shù)借助std::enable_if來實現(xiàn),代碼上多少有點(diǎn)冗余,在這個時候,本節(jié)的主角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); // 調(diào)用int Add(int);
auto res2 = Square(f); // 調(diào)用 float Add(float);
auto res3 = Square(b); // call bool Square(bool);
auto res4 = Square(n); // 成功
return 0;
}
編譯成功。
我們借助一個Square()函數(shù)模板以及更加符合編碼習(xí)慣的if語句就能解決上面的問題,且比使用std::enable_if方式更為優(yōu)雅和符合閱讀習(xí)慣,進(jìn)而提高代碼的可閱讀性。