2017-03-17 58 views
1

我正在研究一個複雜的框架,它使用std::function<>作爲許多函數的參數。通過分析我發現以下一個性能問題。可以將std :: function內聯或者我應該使用不同的方法?

有人可以解釋爲什麼Loop3a太慢嗎?我預計內聯會被使用,時間也會一樣。裝配也一樣。有什麼方法可以提高性能或改變方式嗎? C++ 17是否會以這種方式進行改變?

#include <iostream> 
#include <functional> 
#include <chrono> 
#include <cmath> 

static const unsigned N = 300; 

struct Loop3a 
{ 
    void impl() 
    { 
     sum = 0.0; 
     for (unsigned i = 1; i <= N; ++i) { 
      for (unsigned j = 1; j <= N; ++j) { 
       for (unsigned k = 1; k <= N; ++k) { 
        sum += fn(i, j, k); 
       } 
      } 
     } 
    } 

    std::function<double(double, double, double)> fn = [](double a, double b, double c) { 
     const auto subFn = [](double x, double y) { return x/(y+1); }; 
     return sin(a) + log(subFn(b, c)); 
    }; 
    double sum; 
}; 


struct Loop3b 
{ 
    void impl() 
    { 
     sum = 0.0; 
     for (unsigned i = 1; i <= N; ++i) { 
      for (unsigned j = 1; j <= N; ++j) { 
       for (unsigned k = 1; k <= N; ++k) { 
        sum += sin((double)i) + log((double)j/(k+1)); 
       } 
      } 
     } 
    } 

    double sum; 
}; 


int main() 
{ 
    using Clock = std::chrono::high_resolution_clock; 
    using TimePoint = std::chrono::time_point<Clock>; 

    TimePoint start, stop; 
    Loop3a a; 
    Loop3b b; 

    start = Clock::now(); 
    a.impl(); 
    stop = Clock::now(); 
    std::cout << "A: " << std::chrono::duration_cast<std::chrono::milliseconds>(stop - start).count(); 
    std::cout << "ms\n"; 

    start = Clock::now(); 
    b.impl(); 
    stop = Clock::now(); 
    std::cout << "B: " << std::chrono::duration_cast<std::chrono::milliseconds>(stop - start).count(); 
    std::cout << "ms\n"; 

    return a.sum == b.sum; 
} 

示例輸出使用克++ 5.4用 「-O2 -std = C++ 14」:

A: 1794ms 
B: 906ms 

在探查我可以看到這個內部的許多:

double&& std::forward<double>(std::remove_reference<double>::type&) 
std::_Function_handler<double (double, double, double), Loop3a::fn::{lambda(double, double, double)#1}>::_M_invoke(std::_Any_data const&, double, double, double) 
Loop3a::fn::{lambda(double, double, double)#1}* const& std::_Any_data::_M_access<Loop3a::fn::{lambda(double, double, double)#1}*>() const 
+0

的std ::函數執行類型擦除,從不內聯。 –

+1

恕我直言,在這種情況下,你最好配上一個函數而不是lambda。然後你可以有一個命名的函數類型,而不是'std :: function',編譯器擅長內聯函子。 – NathanOliver

+0

使用lambda代替'std :: function'會更快嗎?即'auto fn = [](double a,double b,double c)...'。 – ilotXXI

回答

7

std::function而不是零運行成本抽象。它是一種類型擦除包裝,當調用operator()時可能會產生類似成本的調用,並且可能會堆分配(這可能意味着每個調用的緩存未命中)

編譯器很可能無法將其內聯

如果你要存儲功能,從而不會引入額外開銷的方式對象和允許編譯器在線它,你應該使用一個模板參數。這並非總是可行,但可能適合您的使用情況。


我寫的有關該主題的文章:
"Passing functions to functions"

它包含了一些基準,顯示多少總成std::function生成相比模板參數和其他解決方案。

+4

有趣的事實:它是在一個小時的第三次,我回答一個問題,這是不必要的* *使用'的std :: function'。我希望它更清楚,它不是一個零成本的抽象,它被命名爲'std :: type_erased_function' ... –

+0

@Vittorrio:我認爲你可以責怪cpp參考網站,當我研究如何存儲我的lambda,這是參考頁面提到的。事實證明,函數指針要快得多,但關於lambdas的頁面並沒有提到你可以將lambda傳遞給其中的一個。 –

+0

@JasonLang:那是因爲你不能。如果它是* captureless * lambda,則只能將lambda轉換爲函數指針。參考頁面指出。 –

4

std::function大致具有虛擬通話開銷。這很小,但如果你的操作更小,它可能很大。

在你的情況下,你在std::function上重複循環,用一組可預測的值調用它,並且可能在它的旁邊沒有任何東西。

我們可以解決這個問題。

template<class F> 
std::function<double(double, double, double, unsigned)> 
repeated_sum(F&& f) { 
    return 
    [f=std::forward<F>(f)] 
    (double a, double b, double c, unsigned count) 
    { 
     double sum = 0.0; 
     for (unsigned i = 0; i < count; ++i) 
     sum += f(a,b,c+i); 
     return sum; 
    }; 
} 

然後

std::function<double(double, double, double, unsigned)> fn = 
    repeated_sum 
    (
    [](double a, double b, double c) { 
     const auto subFn = [](double x, double y) { return x/(y+1); }; 
     return sin(a) + log(subFn(b, c)); 
    } 
); 

現在repeating_function需要double, double, double函數,並返回一個double, double, double, unsigned。這個新函數重複調用前一個函數,每次最後一個座標增加1。

我們再更換impl如下:

void impl() 
{ 
    sum = 0.0; 
    for (unsigned i = 1; i <= N; ++i) { 
     for (unsigned j = 1; j <= N; ++j) { 
      fn(i,j,0,N); 
     } 
    } 
} 

,我們取代「最低級循環」與我們的重複功能的單一調用。

這將通過300的一個因素,基本上都會使它消失減少虛擬調用的開銷。基本上,50%的時間/ 300 = 0.15%的時間(實際上是0.3%,因爲我們將時間減少了2倍,使得貢獻加倍,但是誰在計數十分之一?)

現在在實際情況下,您可能不會用300個相鄰值調用它。但通常有一些模式。

我們上面所做的是移動一些邏輯控制fn如何在fn內被調用。如果您可以做到這一點,您可以考慮刪除虛擬通話開銷。

std::function開銷大部分是可以忽略的,除非你想把它叫做每秒數十億次,我稱之爲「每像素」操作。用「每掃描線」替換這些操作 - 每行相鄰像素 - 開銷停止是一個問題。

這可能需要一些暴露的邏輯的,關於如何功能對象用於「在報頭」。根據我的經驗,仔細選擇您公開的邏輯可以使其相對通用。

最後,請注意,這是可能的內聯std::function和編譯器越來越它更好。但它很難,而且很脆弱。依靠它在這一點上是不明智的。


還有另一種方法。

template<class F> 
struct looper_t { 
    F fn; 
    double operator()(unsigned a, unsigned b, unsigned c) const { 
    double sum = 0; 
    for (unsigned i = 0; i < a; ++i) 
     for (unsigned j = 0; j < b; ++j) 
     for (unsigned k = 0; k < c; ++k) 
      sum += fn(i,j,k); 
    return sum; 
    } 
}; 
template<class F> 
looper_t<F> looper(F f) { 
    return {std::move(f)}; 
} 

現在我們寫活套:而不是僅僅尾隨尺寸

struct Loop3c { 
    std::function<double(unsigned, unsigned, unsigned)> fn = looper(
    [](double a, double b, double c) { 
     const auto subFn = [](double x, double y) { return x/(y+1); }; 
     return sin(a) + log(subFn(b, c)); 
    } 
); 
    double sum = 0; 
    void impl() { 
    sum=fn(N,N,N); 
    } 
}; 

其刪除三維循環的整個操作。

+0

使用模板參數的第二種方法在我看來是重構之前的解決方案,即使我不喜歡它,因爲我較少控制在模板參數中傳遞的內容。我希望不會有隱式轉換和其他內容。 – Radek

+0

@Radek我不明白。你的意思是'looper_t'嗎?在這種情況下,模板參數是一個lambda,你可以通過調用'looper'來控制它。然後''looper_t'被轉換成'std :: function',就像你的代碼中的lambda被轉換成'std :: function'一樣。這就是'std :: function'是* for *的意思。 – Yakk

+0

謝謝,我明白了。幾個月前,我只想避免在各處引用'template '。你知道std :: function 更具可讀性。 – Radek

相關問題