5

私はいくつかの簡単な機能を持っています

int f_0(int);
int f_1(int);
...
int f_n(int);

そして、f_i() を呼び出す for ループがいくつかあります。このループの条件は同じである必要はありません。

for (int i = 0; i < n; i++) {
   ...
   if (condition) {
      int myInt = f_i(); // this is not real implementation but shows the result
                         // I want to achieve
      ... //edit
   }
...
}

これを実装しようとした方法は次のとおりです。

  • for ループを分解し、対応する部分で各関数を呼び出します。これにより、最速のコードが得られますが、これは非常に洗練されておらず、そのようなコードをさらに開発することは困難です。
  • 関数へのポインタ

    typedef int (*Foo) (int);

    Foo fptr[] = { f_0, f_1, ... , f_n };

これは洗練された方法ですが、私の場合、ループを分割するよりも 4.4 遅くなります。関数への定数ポインターは、同様の結果をもたらします。

  • 私の機能をスイッチ機能にカプセル化します。これは、ループを分割するよりも 2.6 遅くなりました。

これを実装するより良い方法はありますか?理想的な解決策はコンパクトなコードを使用するものですが、コンパイラーはループを分割し、計算を最速にします。

私は MSVC 2012 を使用しており、速度を最大化するように最適化を設定してリリース モードで実行しています。

編集:

ここに私のテストコードがあります:

head.h

namespace c {
const int w = 1024;
const int A = w * w;
}

inline int f_0(int pos)  { return (pos - c::w + c::A) % c::A;           }
inline int f_1(int pos)  { return (pos + 1 - c::w + c::A) % c::A;       }
inline int f_2(int pos)  { return (pos + 1) % c::A;                     }
inline int f_3(int pos)  { return (pos + c::w) % c::A;                  }
inline int f_4(int pos)  { return (pos - 1 + c::w) % c::A;              }
inline int f_5(int pos)  { return (pos - 1 + c::A) % c::A;              }

typedef int (*NEIGH_F) (int);
typedef int (* const CNEIGH_F) (int);

const NEIGH_F  fptr[]  = { f_0, f_1, f_2, f_3, f_4, f_5 };
const CNEIGH_F cfptr[] = { f_0, f_1, f_2, f_3, f_4, f_5 };

inline int fswitch(int i, int pos) {
    switch(i) {
    case 0 : return f_0(pos); break;
    case 1 : return f_1(pos); break;
    case 2 : return f_2(pos); break;
    case 3 : return f_3(pos); break;
    case 4 : return f_4(pos); break;
    case 5 : return f_5(pos); break;
    default : return -1; break;
    }
}

main.cpp

#include "head.h"
#include <iostream>
#include <time.h>

int main()
{
    int maxRepeat = 100;

    clock_t startTime = clock();
    double sum = 0;
    for (int repeat = 0; repeat < maxRepeat; repeat++)
        for (int i = 0; i < c::A; i++) {
            sum += f_0(i);
            sum += f_1(i);
            sum += f_2(i);
            sum += f_3(i);
            sum += f_4(i);
            sum += f_5(i);
        }
    std::cout << "normal time:        " << (clock() - startTime)/(double)CLOCKS_PER_SEC
                 << "  sum is: " << sum << std::endl;

    startTime = clock();
    sum = 0;
    for (int repeat = 0; repeat < maxRepeat; repeat++)
        for (int i = 0; i < c::A; i++) {
            for (int j = 0; j < 6; j++)
                sum += fptr[j](i);
        }
    std::cout << "pointer time:       " << (clock() - startTime)/(double)CLOCKS_PER_SEC
                 << "  sum is: " << sum << std::endl;

    startTime = clock();
    sum = 0;
    for (int repeat = 0; repeat < maxRepeat; repeat++)
        for (int i = 0; i < c::A; i++) {
            for (int j = 0; j < 6; j++)
                sum += cfptr[j](i);
        }
    std::cout << "const pointer time: " << (clock() - startTime)/(double)CLOCKS_PER_SEC
                 << "  sum is: " << sum << std::endl;

    startTime = clock();
    sum = 0;
    for (int repeat = 0; repeat < maxRepeat; repeat++)
        for (int i = 0; i < c::A; i++) {
            for (int j = 0; j < 6; j++)
                sum += fswitch(j, i);
        }
    std::cout << "switch time:        " << (clock() - startTime)/(double)CLOCKS_PER_SEC
                 << "  sum is: " << sum << std::endl;
    std::cin.ignore();

    return 0;
}

関数 f_i は実際の実装で使用する関数ですが、実際の実装でのテスト目的のため、ここのループははるかに単純です。質問の 2 番目のコード スニペットに示されている形式の異なるループがいくつかあります。

編集2:

ループの形式は同じままである必要があります。f_i をループに入れる最良の方法を見つけたいだけです。

4

4 に答える 4

4

f_0の代わりにテンプレート関数を使用できますf_1...維持しやすくなります。

template <int N>
void f();

template <>
void f<0>()
{
    printf("f<0>");
}

template <>
void f<1>()
{
    printf("f<1>");
}

int main() {
    f<0>();
    f<1>();
    //f<2>(); // this is compile error
    return 0;
}

ただし、テンプレート引数はコンパイル時の定数として提供する必要があるため、次のような関数を呼び出すことはできませんint i = 0; f<i>()

これを回避するには、switch-case を使用して関数を呼び出すことができます。あまりきれいではありませんが、機能します

void call_f(int i)
{
    switch(i)
    {
        case 0:
            f<0>();
            break;
        case 1:
            f<1>();
            break;
        default:
            // invalid i, report error
            break;
    }
}

ただし、コンパイル時のチェックはありませんi

すべてをまとめる

于 2013-11-05T01:02:36.123 に答える
2

Bryan Chen のテンプレート ベースのソリューションは非常に理にかなっていると思います。保守と理解が容易になります。私はその解決策に賛成しました。

とはいえ、switch ステートメントを使用しないより一般的なソリューションが必要で、すべての条件を「展開」した方法でテストしたい場合は、テンプレートでコンパイル時の再帰を使用できます。

単一の整数引数を取る条件ファンクターに基づいて、3 つの関数でそれを行いました。明らかに、必要に応じて、条件をより単純にしたり、より複雑にしたりできます。

これの中核には、再帰的なテンプレート定義と、再帰を停止するためのテンプレートの特殊化が含まれます。

template <int N>
struct Condition;  // provides bool operator()(int arg)

template <int N>
void f();

template <int N>
void applyFunctions(int arg);

// Specialization placed first for clarity
template <>
void applyFunctions<0>(int arg)
{
  if (Condition<0>()(arg))
  {
    f<0>();
  }
  // End recursion
};

template <int N>
void applyFunctions(int arg)
{
  if (Condition<N>()(arg))
  {
    f<N>();
  }

  applyFunctions<N - 1>(arg);
};

ここにいくつかの出力があります。フレーズは条件チェックで出力[f<i>]されますが、関数呼び出し内で出力されます。わかりやすくするために、印刷出力を揃えました。

Loop
j = 0:                       Is even. [f<1>]       Always true. [f<0>]
j = 1:                                             Always true. [f<0>]
j = 2:  Is prime. [f<2>]     Is even. [f<1>]       Always true. [f<0>]
j = 3:  Is prime. [f<2>]                           Always true. [f<0>]
j = 4:                       Is even. [f<1>]       Always true. [f<0>]
j = 5:  Is prime. [f<2>]                           Always true. [f<0>]
j = 6:                       Is even. [f<1>]       Always true. [f<0>]
j = 7:  Is prime. [f<2>]                           Always true. [f<0>]
j = 8:                       Is even. [f<1>]       Always true. [f<0>]
j = 9:                                             Always true. [f<0>]
j = 10:                      Is even. [f<1>]       Always true. [f<0>]

完全なプログラムは以下です。本当に何かクールなことをしたい場合は、結果のコードのインクルードがコンパイル時に決定されるように、Condition構造体に何らかの方法で計算されるメンバー変数を持たせることができます。constexprそれがあなたにとって何の意味もない場合は、おそらくテンプレート、テンプレートのインスタンス化、およびメタプログラミングについて読みたいと思うでしょう。

#include <iostream>
#include <iomanip>

static int fw = 20;

template <int N>
struct Condition;

template <int N>
void f();


// Specialization 0
template <>
struct Condition<0>
{
  bool operator() (int arg)
  {
    std::cout << std::setw(fw) << " Always true. ";
    return true;
  }
};

template <>
void f<0>()
{
  std::cout << "[f<0>]";
}

// Specialization 1
template <>
struct Condition<1>
{
  bool operator() (int arg)
  {
    bool isEven = (arg % 2 == 0);
    if (isEven)
      std::cout << std::setw(fw) << " Is even. ";
    else 
      std::cout << std::setw(fw) << " ";
    return isEven;
  }
};

template <>
void f<1>()
{
  std::cout << "[f<1>]";
}


// Specialization 2
template <>
struct Condition<2>
{
  bool operator() (int arg)
  {
    bool isPrime = (arg == 2 || arg == 3 || arg == 5 || arg == 7);
    if (isPrime)
      std::cout << std::setw(fw) << " Is prime. ";
    else 
      std::cout << std::setw(fw) << " ";
    return isPrime;
  }
};

template <>
void f<2>()
{
  std::cout<< "[f<2>]";
}


template <int N>
void applyFunctions(int arg);

template <>
void applyFunctions<0>(int arg)
{
  if (Condition<0>()(arg))
  {
    f<0>();
  }
  // End recursion
};

template <int N>
void applyFunctions(int arg)
{
  if (Condition<N>()(arg))
  {
    f<N>();
  }

  applyFunctions<N - 1>(arg);
};


int main()
{
  applyFunctions<2>(4);

  std::cout << std::endl << "Loop" << std::endl;
  for (int j = 0; j < 11; ++j)
  {
    std::cout << "j = " << j << ": ";
    applyFunctions<2>(j);
    std::cout << std::endl;
  }
}
于 2013-11-05T06:57:07.007 に答える
2

次の 2 つの調整は、プログラムからの結果の出力を根本的に変更します (クリーンなコンパイル コードに感謝します!)。これらは、パフォーマンスの最適化には、ビルド時と実行時の不確実性との間に明確なトレードオフがあることを示しています。どの関数を呼び出すか、またはどのターゲット マシンで実行するかを知っていれば、より最適なコードを書くことができます。

ポインターを介した関数呼び出しでは、関数呼び出しをインライン化しない代わりに、実行時に関数を柔軟に呼び出すことができます。次の呼び出しを変更すると、ポインター時間が通常の時間と等しくなります。

normal time:        1.36  sum is: 3.29853e+14
pointer time:       1.36  sum is: 3.29853e+14
const pointer time: 1.35  sum is: 3.29853e+14
switch time:        1.14  sum is: 3.29853e+14

変更はループ内で関数呼び出しを展開していたため、次のようになります。

   sum += fptr[1](i);
   sum += fptr[2](i);
   sum += fptr[3](i);
   sum += fptr[4](i);
   sum += fptr[5](i);

fswitch()fswitch()おそらく、内部をインライン化すると、キャッシュされる一連の命令が作成されるため、表示された場合の通常よりも高速です。おそらく、必要な専門知識を持つ誰かが、生成された実行可能ファイルの逆アセンブルでこれを実証できるでしょう。私のテストでは、スイッチ機能を少し拡大しました (switch以下に示すように、それらを複製して二重分岐を使用)。通常よりも約 4 倍遅く実行されることがわかりました。

normal time:        2.35  sum is: 6.59706e+14
pointer time:       2.35  sum is: 6.59706e+14
const pointer time: 2.34  sum is: 6.59706e+14
switch time:        9.61  sum is: 6.59706e+14

変更点は次のとおりです。

case 6 : return f_0(pos); break;
case 7 : return f_1(pos); break;
case 8 : return f_2(pos); break;
case 9 : return f_3(pos); break;
case 10 : return f_4(pos); break;
case 11 : return f_5(pos); break;

...

for (int j = 0; j < 12; j++)
    sum += fswitch(j, i);

...

const NEIGH_F  fptr[]  = { f_0, f_1, f_2, f_3, f_4, f_5, f_0, f_1, f_2, f_3, f_4, f_5 };
const CNEIGH_F cfptr[] = { f_0, f_1, f_2, f_3, f_4, f_5, f_0, f_1, f_2, f_3, f_4, f_5 };

...

for (int j = 0; j < 12; j++)
    sum += fptr[j](i);

...

于 2013-11-05T07:53:13.243 に答える