2013-03-26 38 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然后这些会产生不同的结果。

相关问题