2013-03-26 24 views
5

我想基準用C++編寫的算法的許多(約25個)變體。處理用於創建算法的多個版本的#ifdef's

我實現使用三種方法的組合的這些變化:

  1. 複製代碼,並使得將複製的版本

  2. 微小變化繼承使用#ifdef S上的基算法類

  3. 在片段代碼之間切換

選項1和2產生的變化是可以的,因爲我可以選擇在配置文件中運行算法的哪種變體。然後,我可以遍歷不同的配置文件並保留「配置:結果」對的記錄 - 保留這些記錄對我的工作非常重要。

我目前遇到#ifdef問題,因爲我必須編譯多個版本的代碼才能訪問這些變體,這使得運行自動化實驗腳本和保持結果的準確記錄變得更加困難。然而,#ifdef是非常有用的,因爲如果我在代碼的一個副本中發現錯誤,那麼我不必記得在多個副本中糾正這個錯誤。

#ifdef的小號擴大我創建由兩個複製代碼和子類爲24個總變化(4個變化爲每個基本變化)6度的變化。

下面是一個例子 - 主要是我現在用的是#ifdef,以免發生複製太多的代碼:

.... 

    double lasso_gam=*gamma; 
    *lasso_idx=-1; 
    for(int aj=0;aj<(int)a_idx.size();aj++){ 
     int j=a_idx[aj]; 
     assert(j<=C*L); 
     double inc=wa[aj]*(*gamma)*signs[aj]; 
     if((beta_sp(j)>0 && beta_sp(j)+inc<0) 
#ifdef ALLOW_NEG_LARS 
      || (beta_sp(j)<0 && beta_sp(j)+inc>0) 
#else 
      || (beta_sp(j)==0 && beta_sp(j)+inc<0) 
#endif 
      ){ 
      double tmp_gam=-beta_sp(j)/wa[aj]*signs[aj]; 

      if(tmp_gam>=0 && tmp_gam<lasso_gam) { 
       *lasso_idx=aj; 
       *next_active=j; 
       lasso_gam=tmp_gam; 
      } 
     } 
    } 

    if(lasso_idx>=0){ 
     *gamma=lasso_gam; 
    } 

    .... 

問題:什麼是允許算法的多種變化的最佳方式,目前由#ifdef指定,在給定配置文件的情況下運行,該配置文件指定運行算法的哪個變體。

理想情況下,我想只編譯一次代碼,並在運行時使用配置文件選擇算法變體。

回答

1

如果您#if s的各地分散的,在這裏修改一行代碼或有那麼把你的所有#if s轉換基於傳遞到函數的哪個版本運行一個枚舉if S和希望編譯器做了偉大的在優化工作。希望它會產生與定義函數幾乎相同的代碼,除了使用單個運行時條件來決定運行哪個函數。沒有保證。

如果您是#if將算法中的代碼塊拆分成更小的函數,您可以調用整個算法的不同實現。這顯然不切實際,如果你的#if是如此侵入性,你會盡快結束50個功能。

+0

這是我目前正在考慮的事情。 '如果'會在最內層的循環中,所以我擔心性能受到影響。不過,我想優化級別會有所作爲。 – user1149913 2013-03-26 18:46:44

0

如果將算法本身放置在具有相同接口的類中,則可以將它們作爲模板參數傳遞到使用該算法的位置。

class foo { 
public: 
    void do_something() { 
    std::cout << "foo!" << std::endl; 
    } 
} 

class bar { 
public: 
    void do_something() { 
    std::cout << "bar!" << std::endl; 
} 

template <class meh> 
void something() { 
    meh algorithm; 
    meh.do_something(); 
} 

int main() { 
    std::vector<std::string> config_values = get_config_values_from_somewhere(); 
    for (const austo& config : config_values) { // c++11 for short notation 
    switch (config) { 
     case "foo": 
     something<foo>(); 
     break; 
     case "bar": 
     something<bar>(); 
     break; 
     default: 
     std::cout << "undefined behaviour" << std::endl; 
    } 
    } 
} 

這樣,您可以同時使用不同的行爲,並通過名稱區分它們。另外,如果你不使用其中的一個,它將在編譯時被優化器刪除(不是你的問題)。

讀取配置文件時,您只需要一個工廠(或類似的)來創建在使用該算法之前應該使用算法的對象/函數的正確實例。

編輯:添加了基本開關。

+0

我的問題實際上並不是要創建大量幾乎完全相同的'do_something'函數,而不是如何在這些函數中進行選擇。 – user1149913 2013-03-26 18:59:57

+0

@ user1149913增加了一個開關,讓我的觀點更加清晰。 – scones 2013-03-26 19:07:25

0

你還沒有提到你正在使用的編譯器,但是你可以在命令行上爲它們中的任何一個設置#defines。在gcc中,您只需添加-D MYTESTFOO即可定義MYTESTFOO。這將使#定義要走的路 - 無需更改代碼進行傳播,當然,每個測試都會有不同的編譯代碼,但它應該很容易自動化。

4

您可以用(可能還有其他的)模板參數像這樣增加自己的算法:

enum class algorithm_type 
{ 
    type_a, 
    type_b, 
    type_c 
}; 

template <algorithm_type AlgorithmType> 
void foo(int usual, double args) 
{ 
    std::cout << "common code" << std::endl; 

    if (AlgorithmType == algorithm_type::type_a) 
    { 
     std::cout << "doing type a..." << usual << ", " << args << std::endl; 
    } 
    else if (AlgorithmType == algorithm_type::type_b) 
    { 
     std::cout << "doing type b..." << usual << ", " << args << std::endl; 
    } 
    else if (AlgorithmType == algorithm_type::type_c) 
    { 
     std::cout << "doing type c..." << usual << ", " << args << std::endl; 
    } 

    std::cout << "more common code" << std::endl; 
} 

現在,你可以通過這個模板參數選擇您的行爲:

foo<algorithm_type::type_a>(11, 0.1605); 
foo<algorithm_type::type_b>(11, 0.1605); 
foo<algorithm_type::type_c>(11, 0.1605); 

的類型,是一個常量表達式產生一個編譯時間決定的分支(也就是說,其他的已知是死代碼並被刪除)。事實上,你的編譯器應該警告你這個問題(你如何處理這個問題取決於你)。

但你仍然可以派遣關運行時值就好:

#include <stdexcept> 

void foo_with_runtime_switch(algorithm_type algorithmType, 
          int usual, double args) 
{ 
    switch (algorithmType) 
    { 
    case algorithm_type::type_a: 
     return foo<algorithm_type::type_a>(usual, args); 
    case algorithm_type::type_b: 
     return foo<algorithm_type::type_b>(usual, args); 
    case algorithm_type::type_c: 
     return foo<algorithm_type::type_c>(usual, args); 
    default: 
     throw std::runtime_error("wat"); 
    } 
} 

foo_with_runtime_switch(algorithm_type::type_a, 11, 0.1605); 
foo_with_runtime_switch(algorithm_type::type_b, 11, 0.1605); 
foo_with_runtime_switch(algorithm_type::type_c, 11, 0.1605); 

算法的內部保持不變(枯枝淘汰,同樣的優化),你是如何到達那裏發生了變化。 (請注意,可以將枚舉的概念進行概括,以便該開關自動生成;如果發現自己的變體極少,則可能很好學習)。

當然,您仍然可以將#define作爲特定算法默認:

#define FOO_ALGORITHM algorithm_type::type_a 

void foo_with_define(int usual, double args) 
{ 
    return foo<FOO_ALGORITHM>(usual, args); 
} 

foo_with_define(11, 0.1605); 

所有這些一起給你所有三個優點,沒有重複。

在實踐中,你可以將所有三個重載:重要的是知道在編譯時使用哪種算法的用戶,需要在運行時選擇它的用戶,以及那些只需要默認值的用戶通過項目範圍#define):

// foo.hpp 

enum class algorithm_type 
{ 
    type_a, 
    type_b, 
    type_c 
}; 

// for those who know which algorithm to use 
template <algorithm_type AlgorithmType> 
void foo(int usual, double args) 
{ 
    std::cout << "common code" << std::endl; 

    if (AlgorithmType == algorithm_type::type_a) 
    { 
     std::cout << "doing type a..." << usual << ", " << args << std::endl; 
    } 
    else if (AlgorithmType == algorithm_type::type_b) 
    { 
     std::cout << "doing type b..." << usual << ", " << args << std::endl; 
    } 
    else if (AlgorithmType == algorithm_type::type_c) 
    { 
     std::cout << "doing type c..." << usual << ", " << args << std::endl; 
    } 

    std::cout << "more common code" << std::endl; 
} 

// for those who will know at runtime 
void foo(algorithm_type algorithmType, int usual, double args) 
{ 
    switch (algorithmType) 
    { 
    case algorithm_type::type_a: 
     return foo<algorithm_type::type_a>(usual, args); 
    case algorithm_type::type_b: 
     return foo<algorithm_type::type_b>(usual, args); 
    case algorithm_type::type_c: 
     return foo<algorithm_type::type_c>(usual, args); 
    default: 
     throw std::runtime_error("wat"); 
    } 
} 

#ifndef FOO_ALGORITHM 
    // chosen to be the best default by profiling 
    #define FOO_ALGORITHM algorithm_type::type_b 
#endif 

// for those who just want a good default 
void foo(int usual, double args) 
{ 
    return foo<FOO_ALGORITHM>(usual, args); 
} 

當然,如果一些實現類型總是比其他一些更糟糕,擺脫它。但是如果你發現有兩個有用的實現,那麼保持這種方式並沒有什麼壞處。

+0

+1對我來說,這是一個不錯的解決方案。 – 2013-03-26 18:50:53

+0

這不適用於我的情況。理想情況下,我想編譯一次並獲得由'ifdef'提供的算法的所有版本,然後根據配置文件在運行時選擇alg版本。 – user1149913 2013-03-26 18:57:41

+0

@ user1149913:擴展你的問題,我沒有選擇它。你通過'ifdef'定義了多個?這是如何運作的? – GManNickG 2013-03-26 19:03:52

4

如果你有多個版本#ifdef S,它通常是打造多個可執行文件,有你的配置腳本決定哪些可執行文件(一個或多個)基準測試時運行。然後,您有規則在Makefile來構建各種配置:

%-FOO.o: %.cc 
     $(CXX) -c $(CFLAGS) -DFOO -o [email protected] $< 

%-BAR.o: %.cc 
     $(CXX) -c $(CFLAGS) -DBAR -o [email protected] $< 

test-FOO: $(SRCS:%.cc=%-FOO.o) 
     $(CXX) $(LDFLAGS) -DFOO -o [email protected] $^ $(LDLIBS) 
+0

我一直在考慮這樣做,但它會爲我的實驗腳本增加一層額外的複雜性。 (我正在集羣上運行這些程序)。 – user1149913 2013-03-26 19:28:22

+0

與其他解決方案不同,這也具有輕鬆允許測試不同編譯器指令(例如openmp pragmas)以及多選代碼和預處理器選項的排列的優點。 – 2015-10-15 23:16:43

0

一種方式將不包含在可執行文件中預處理指令,並做到這一點正是如此:

#define METHOD METHOD1 
int Method1() { return whatever(); }; 
#undef METHOD 

#define METHOD METHOD2 
int Method2() { return whatever(); }; 
#undef METHOD 

假設whatever是依賴METHOD然後這些會產生不同的結果。

相關問題