5

私はC++で書かれたアルゴリズムの多く(約25)のバリエーションをベンチマークしようとしています。

これらのバリエーションは、次の3つの方法を組み合わせて実装しました。

  1. コードをコピーし、コピーしたバージョンに小さな変更を加える

  2. 基本アルゴリズムクラスのサブクラス化

  3. sを使用して#ifdefコードスニペットを切り替える

オプション1と2から生じるバリエーションは、構成ファイルで実行するアルゴリズムのバリエーションを選択できるため、問題ありません。次に、さまざまな構成ファイルを反復処理して、「configuration:results」ペアの記録を保持できます。これらの記録を保持することは、私の作業にとって非常に重要です。

#ifdefこれらのバリエーションにアクセスするには、コードの複数のバージョンをコンパイルする必要があり、自動化された実験スクリプトを実行して結果の正確な記録を保持することが非常に困難になるため、現在、sに問題があります。#ifdefただし、コードの1つのコピーで間違いを見つけた場合、複数のコピーでこの間違いを修正することを覚えておく必要がないため、sは非常に便利です。

sは#ifdef、コードのコピーとサブクラス化の両方によって作成した6つのバリエーションを、合計24のバリエーション(基本的なバリエーションごとに4つのバリエーション)に拡張します。

次に例を示します。ほとんどの場合、#ifdefコードの複製が多すぎないようにsを使用しています。

    ....

    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実行するアルゴリズムのバリエーションを指定する構成ファイルを指定して、現在sで指定されているアルゴリズムの複数のバリエーションを実行できるようにするための最良の方法は何ですか。

理想的には、コードを1回だけコンパイルし、構成ファイルを使用して実行時にアルゴリズムのバリエーションを選択したいと思います。

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

これらすべてを一緒に使用すると、繰り返しなしで3つすべての利点が得られます。

実際には、3つすべてをオーバーロードとして使用できます。1つはコンパイル時に使用するアルゴリズムを知っているユーザー、実行時にそれを選択する必要があるユーザー、およびデフォルト(プロジェクトを介してオーバーライドできます)が必要なユーザー向けです。ワイド#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);
}

もちろん、一部の実装タイプが常に他のタイプよりも悪い場合は、それを取り除きます。しかし、2つの有用な実装があることがわかった場合、この方法で両方を維持しても害はありません。

于 2013-03-26T18:48:42.470 に答える
4

sを使用して複数のバージョンがある場合は#ifdef、通常、複数の実行可能ファイルを作成し、ベンチマーク時に実行する実行可能ファイルを構成スクリプトに決定させるのが最善です。次に、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に#if変換し、コンパイラが最適化で優れた仕事をすることを期待します。ifうまくいけば、実行するものを決定する単一の実行時条件を除いて、関数を複数回定義したのとほぼ同じコードが生成されます。約束はできません。

アルゴリズムでコードのブロックを使用している場合は#if、アルゴリズムを、アルゴリズム全体のさまざまな実装が呼び出すことができる小さな関数に分割します。#ifただし、50の関数を使用するほど煩わしい場合、これは明らかに実用的ではありません。

于 2013-03-26T18:39:50.820 に答える
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;
    }
  }
}

このようにして、さまざまな動作を同時に使用し、名前で区別することができます。また、それらの1つを使用しない場合は、コンパイル時にオプティマイザーによって削除されます(ただし、問題はありません)。

構成ファイルを読み取るときは、アルゴリズムを使用する前にアルゴリズムを使用する必要があるオブジェクト/関数の正しいインスタンスを作成するためのファクトリ(または同様のもの)が必要です。

編集:基本スイッチを追加しました。

于 2013-03-26T18:40:04.597 に答える
0

使用しているコンパイラについては言及していませんが、コマンドラインで#definesを設定できます。gccでは、-D MYTESTFOOMYTESTFOOを定義するために追加するだけで済みます。これにより、#definesが進むべき道を定義します。伝播するコードの変更はありません。確かに、テストごとに異なるコンパイル済みコードがありますが、自動化は簡単です。

于 2013-03-26T18:42:24.193 に答える
0

1つの方法は、実行可能ファイルにプリプロセッサディレクティブを含めないため、次のようにします。

#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 に答える