這篇主要是簡單地針對 C 裡面,針對如何傳遞一個函式(function),以及各種方法對於效能的影響,做個簡單的測試。
基本上,Heresy 自己知道在 C 標準裡面,可以用來傳遞函式的方法,包括了下面三種:
- C 的 Function Pointer
- C 的 Template function
- C STL Function Object
下面就是 Heresy 用來測試的程式:
Function Pointer
Heresy 之前也有在《在 C 裡傳遞、儲存函式 Part 1:Function Pointer》介紹過,算是一個很典型的方法,但是由於語法上的關係,Heresy 個人不是很喜歡這樣的寫法。
這邊用的測試函式如下:
double(*func)(double, double))
{
double dRes = 0;
for (size_t i = 0; i < uSize; i)
{
dRes = func(pD1[i], pD2[i]);
}
return dRes;
}
Template Function
基本上就是透過 template 的形式,直接把函式當成參數傳遞到另一個函式裡面,之前在《在 C 裡傳遞、儲存函式 Part 2:Function Object》一文內也有介紹過。
這邊用的測試函式如下:
double Templeat_Func(size_t uSize, double* pD1, double* pD2,
_FUNC func)
{
double dRes = 0;
for (size_t i = 0; i < uSize; i)
{
dRes = func(pD1[i], pD2[i]);
}
return dRes;
}
STL Function Object
前面兩個都是語言本身提供的功能,而這個則是透過 STL 函式庫提供的功能;Heresy 之前在《在 C 裡傳遞、儲存函式 Part 3:Function Object in TR1》也有介紹過。
這邊用的函式如下:
std::function<double(double,double)> func )
{
double dRes = 0;
for (size_t i = 0; i < uSize; i)
{
dRes = func( pD1[i], pD2[i] );
}
return dRes;
}
其他說明
基本上,上面三個函式在做的事情都是一樣的,只是傳入 func 這個 callable object 的方式、型別不同而已。而實際上,要使用的方法,是完全相同的~如果是把 lambda expression(介紹)傳進去的話,程式就是:
double res1 = Pointer_Func(uSize, pD1, pD2, func);
double res2 = STL_Func(uSize, pD1, pD2, func);
double res3 = Pointer_Func(uSize, pD1, pD2, func);
除了 lambda expression 外,也可以把傳統的函式傳進去,下面就是函式的定義:
{
return d1 d2;
}
使用方法則是一樣,只要把上面的 func 換成 fadd 就可以了。
而如果針對 template function 和 STL function object 的話,也可以把有 call operator(operator())的類別物件來做傳遞,下面就是類別的定義:
{
public:
double operator()(double d1, double d2)
{
return d1 d2;
}
};
在呼叫的時候,則是要把一個 CAdd 的物件當作參數傳入,使用方法基本上就是:
基本上的東西大概就是這樣了。
測試結果
而在測試的時候,為了凸顯差異,所以刻意把資料變大、同時也多跑幾次來做測試;完整的程式,可以參考 Heresy 放在 GitHub 上的檔案(連結)。
測試的部分,Heresy 是在 Windows 上使用 Microsoft Visual C 2013 update 3(MSVC12),並在 Gentoo Linux 上以 gcc 4.8.3 和 clang 3.4.2 來做測試;編譯參數都只加上「/O2」做最佳化。
MSVC 的測試結果數據如下:
lambda
|
function
|
class
|
|
---|---|---|---|
function pointer
|
1312 ms
|
1303 ms
|
N/A
|
template
|
1313 ms
|
1291 ms
|
1309 ms
|
STL function object
|
2759 ms
|
3389 ms
|
2791 ms
|
從結果可以看到,function pointer 和 template 的效能差異不大,傳哪種類型的函式物件進去的影響也沒有非常地大。但是,STL function object 的效率卻明顯地較差!所需的時間基本上都是兩倍以上,而如果是傳傳統的 function 進去,所需時間甚至會要到近三倍…
clang 的測試結果則如下表。這邊要提醒一下,由於 Windows 和 Gentoo Linux 的硬體並不相同,所以請不要直接比較倆的時間。
lambda
|
function
|
class
|
|
---|---|---|---|
function pointer
|
1431 ms
|
1432 ms
|
N/A
|
template
|
1432 ms
|
1431 ms
|
1431 ms
|
STL function object
|
3356 ms
|
3371 ms
|
3354 ms
|
可以看到,clang 的表現和 MSVC 基本上有點類似:在 function pointer 和 template 的部分,雖然效能有些許的影響,但是變化都不算大;不過 STL function object 的效能雖然明顯地較差,但是在搭配傳統的函式來用的時候,卻沒有那麼明顯地變差。
而 gcc 的結果,個人覺得算滿有趣的…他的測試數據如下:
lambda
|
function
|
class
|
|
---|---|---|---|
function pointer
|
2974 ms
|
2974 ms
|
N/A
|
template
|
2975 ms
|
2974 ms
|
2974 ms
|
STL function object
|
3044 ms
|
3331 ms
|
3043 ms
|
首先,他在 function pointer 和 template 的部分效能很一致,和 MSVC 以及 clang 一樣。這點沒什麼,但是這個測試是和 clang 在同一台電腦跑的,可是所有測試的時間,都大概是 clang 的兩倍!這是代表 gcc 產生的程式效率沒有 clang 好、而且差異大到這個程度嗎?這點可能就還得再測看看了~
其次,雖然 gcc 在 STL function object 的效率也有變差,但是和 MSVC / clang 比起來,他的變差的幅度明顯地較低!以 lambda 和 class 來說,他只多花了 2% 的時間,而更慢的 function 也不過 12%…這樣的測試結果,和 MSVC / clang 的動輒兩三倍的時間相比,可以說是完全不同的樣子啊!這點就不確定是 STL 實作不同造成的影響,還是編譯器最佳化的方式不同了…
(補充一下,如果 gcc 不開「-O2」最佳化的話,STL function object 也是會明顯變慢很多的)
結論
測試完了,這邊根據上面測試的數據,給一些簡單的結論吧~
-
STL 的 function object 效率明顯比較差,在 MSVC 和 clang 下,速度可能只有 template 和 function pointer 的一半以下,所以請審慎評估是否要使用。
-
不過這個影響對 gcc 來說不算大。
-
-
STL function object 搭配傳統的 function 使用時,對於效能的影響會更大;尤其在 MSVC 的影響會更嚴重。
-
以這次的測試程式來說,gcc 建置出來的程式效能遠低於 clang。
所以,看來如果是在效能需求很大、影響會很嚴重的程式碼區段,還是不要用 STL 的 function object 比較好…
(但是 Heresy 實在是很討厭 function pointer 的寫法啊! ><)