5

我正在尝试对用 C++ 编写的算法的许多(大约 25 个)变体进行基准测试。

我使用三种方法的组合来实现这些变体:

  1. 复制代码并对复制的版本进行细微更改

  2. 子类化基本算法类

  3. 使用#ifdefs 在代码片段之间切换

选项 1 和 2 产生的变体是可以的,因为我可以选择算法的哪个变体在配置文件中运行。然后我可以遍历不同的配置文件并记录“配置:结果”对 - 保存这些记录对我的工作非常重要。

我目前遇到了#ifdefs 的问题,因为我必须编译多个版本的代码才能访问这些变体,这使得运行自动化实验脚本和保持准确的结果记录变得更加困难。然而#ifdef,s 非常有用,因为如果我在一个代码副本中发现错误,那么我不必记住在多个副本中更正这个错误。

#ifdefs 扩展了我通过复制代码和子类创建的六个变体,总共有 24 个变体(每个基本变体 4 个变体)。

这是一个示例 - 主要是我使用#ifdefs 来避免复制太多代码:

    ....

    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;
    }

    ....

问题:在给定配置文件的情况下,允许运行当前由#ifdefs 指定的算法的多个变体的最佳方法是什么,该配置文件指定要运行算法的哪个变体。

理想情况下,我只想编译一次代码,并在运行时使用配置文件选择算法变体。

4

6 回答 6

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);
}

当然,如果某些实现类型总是比其他实现类型更差,那就去掉它。但是,如果您发现有两个有用的实现,那么以这种方式保留两者并没有什么坏处。

于 2013-03-26T18:48:42.470 回答
4

如果您有多个带有#ifdefs 的版本,通常最好构建多个可执行文件并让您的配置脚本决定在基准测试时运行哪些可执行文件。然后,您的 Makefile 中有规则来构建各种配置:

%-FOO.o: %.cc
        $(CXX) -c $(CFLAGS) -DFOO -o $@ $<

%-BAR.o: %.cc
        $(CXX) -c $(CFLAGS) -DBAR -o $@ $<

test-FOO: $(SRCS:%.cc=%-FOO.o)
        $(CXX) $(LDFLAGS) -DFOO -o $@ $^ $(LDLIBS)
于 2013-03-26T19:02:45.807 回答
1

如果您#if的 s 分散在各处并在这里或那里更改一行代码,然后根据传递给要运行变体的函数的枚举将所有#ifs 转换为s,并希望编译器在优化方面做得很好。if希望它会生成与多次定义函数几乎相同的代码,除了使用单个运行时条件来决定运行哪个。没有承诺。

如果您#if在算法中使用代码块,则将算法拆分为更小的函数,整个算法的不同实现可以调用这些函数。#if如果您的 s 非常具有侵入性,以至于您最终会得到 50 个函数,这显然是不切实际的。

于 2013-03-26T18:39:50.820 回答
0

一种方法是不要在可执行文件中包含预处理器指令,并这样做:

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

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

假设whatever依赖于METHOD那么这些会产生不同的结果。

于 2013-03-26T19:14:18.907 回答
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;
    }
  }
}

这样,您可以同时使用不同的行为,并通过它们的名称来区分它们。此外,如果您不使用其中之一,优化器将在编译时将其删除(但不在您的问题中)。

读取配置文件时,您只需要一个工厂(或类似的)在使用算法之前创建应该使用算法的对象/函数的正确实例。

编辑:添加基本开关。

于 2013-03-26T18:40:04.597 回答
0

你没有提到你正在使用什么编译器,但是你可以在命令行上为它们中的任何一个设置#defines。在 gcc 中,您只需要添加-D MYTESTFOO以定义 MYTESTFOO。这将使#defines 成为可行的方法 - 无需更改要传播的代码,当然,每个测试都会有不同的编译代码,但它应该很容易自动化。

于 2013-03-26T18:42:24.193 回答