9

forループでのカウントダウンは、実際には実行時により効率的で高速であると私は信じています(いくつかの調査結果から)。私の完全なソフトウェア コードは C++ です

私は現在これを持っています:

for (i=0; i<domain; ++i) {

私の「i」は unsigned resgister int であり、「domain」も unsigned int です

forループでは、iは配列を通過するために使用されます。

array[i] = do stuff

これをカウントダウンに変換すると、ルーチンの期待される/正しい出力が台無しになります。

答えは非常に些細なことだと想像できますが、頭を悩ませることはできません。

更新: 'do stuff' は、前または後の反復に依存しません。for ループ内の計算は、i の反復に対して独立しています。(それが理にかなっていることを願っています)。

更新: for ループでランタイムの高速化を達成するには、カウント ダウンし、その場合は int を宣言するときに署名されていない部分を削除しますか、または他の方法はありますか?

助けてください。

4

14 に答える 14

39

符号なしカウンターを使用して逆方向にループする正しい方法は 1 つだけです。

for( i = n; i-- > 0; )
{
    // Use i as normal here
}

ここにトリックがあります。最後のループ反復では、ループの先頭に i = 1 があり、1 > 0 であるため i-- > 0 が通過し、ループ本体で i = 0 になります。次の反復では、i == 0 であるため i-- > 0 は失敗します。したがって、後置デクリメントがカウンターをロールオーバーしても問題ありません。

私が知っている非常に明白ではありません。

于 2009-04-30T00:20:48.043 に答える
12

あなたには問題がないように見えるので、これはあなたの問題に対する答えではありません。

この種の最適化はまったく無関係であり、コンパイラーに任せる必要があります (実行する場合)。

プログラムのプロファイルを作成して、for ループがボトルネックであることを確認しましたか? そうでない場合は、これについて心配するのに時間を費やす必要はありません。さらに言えば、「i」を「レジスタ」int として持つことは、パフォーマンスの観点からは意味がありません。

問題のドメインを知らなくても、逆ループ手法と "register" int カウンターの両方がプログラムのパフォーマンスにほとんど影響を与えないことを保証できます。「時期尚早の最適化は諸悪の根源」であることを忘れないでください。

とはいえ、全体的なプログラム構造、使用されるデータ構造とアルゴリズム、リソースの使用率などについて考えることに最適化の時間を費やすほうがよいでしょう。

于 2009-04-29T23:47:44.783 に答える
4

カウントアップやカウントダウンは関係ありません。より速くできるのは、ゼロに向かってカウントすることです。Michael の答えは、その理由を示しています — x86 では、多くの命令の暗黙的な副作用としてゼロとの比較が行われるため、カウンターを調整した後は、明示的な比較を行う代わりに、結果に基づいて分岐するだけです。(他のアーキテクチャもそうするかもしれませんが、私にはわかりません。)

Borland の Pascal コンパイラは、その最適化を実行することで有名です。コンパイラはこのコードを次のように変換します。

for i := x to y do
  foo(i);

これに似た内部表現に:

tmp := Succ(y - x);
i := x;
while tmp > 0 do begin
  foo(i);
  Inc(i);
  Dec(tmp);
end;

(最適化がループの結果に影響を与えるからではなく、デバッガーがカウンター変数を正しく表示しないため、悪名高いと言います。プログラマーが を調べると、デバッガーは代わりにiの値を表示することがあります。tmpそれらのループは逆方向に実行されています。)

Incor命令を追加してもDec、実行時間の点では、明示的な比較を行うよりもネットで勝っているという考えです。実際にその違いに気付くことができるかどうかは議論の余地があります.

ただし、変換は、変換が価値があると見なされたかどうかに基づいて、コンパイラが自動的に行うものであることに注意してください。通常、コンパイラはあなたよりもコードの最適化に優れています。

とにかく、あなたは Pascal ではなく C++ について尋ねました。C++ の "for" ループは、Pascal の "for" ループほど最適化を適用するのは簡単ではありません。パスカルのループの境界は、ループが実行される前に常に完全に計算されるためです。一方、C++ のループは、停止条件とループに依存することがあります。コンテンツ。C++ コンパイラは、特定のループが Pascal ループが無条件に適用できる種類の変換の要件に適合するかどうかを判断するために、ある程度の静的解析を行う必要があります。C++ コンパイラが分析を行う場合、同様の変換を行うことができます。

自分でそのようにループを書くことを妨げるものは何もありません:

for (unsigned i = 0, tmp = domain; tmp > 0; ++i, --tmp)
  array[i] = do stuff

これを行うと、コードの実行が速くなる場合があります。さっきも言ったけど、多分気付かないよね。このように手動でループを配置することで支払うより大きなコストは、コードが確立されたイディオムに従わなくなることです。あなたのループは完全に普通の "for" ループですが、もはや1 つのようには見えません— 2 つの変数があり、それらは反対方向にカウントされ、そのうちの 1 つはループ本体でさえ使用されていません — だから、あなたのコード (1 週間後、1 ヶ月後、または 1 年後、達成したいと思っていた「最適化」を忘れたあなたを含む) は、ループが実際に通常のループであることを自分自身に証明するために余分な努力を費やす必要があります。変装した。

(上記のコードでは、符号なしの変数を使用していて、ゼロでラップアラウンドする危険がないことに気付きましたか? 2 つの別個の変数を使用すると、それが可能になります。)

これらすべてから取り除かなければならない 3 つのこと:

  1. オプティマイザーにその仕事をさせてください。全体として、あなたよりも優れています。
  2. 通常のコードを通常のように見せて、特別なコードが競合してレビュー、デバッグ、または保守する人々の注意を引く必要がないようにします。
  3. テストとプロファイリングで必要であることが示されるまで、パフォーマンスの名の下に派手なことをしないでください。
于 2009-04-30T02:44:16.637 に答える
3

では、カウントダウンがより効率的であることを「読んで」いますか?プロファイラーの結果とコードを見せてもらえない限り、これを信じるのは非常に難しいと思います。場合によっては買えますが、基本的には買えません。これは時期尚早の最適化の典型的なケースのように思えます。

「register int i」に関するあなたのコメントも非常に重要です。今日では、コンパイラーは常に、レジスターの割り当て方法をユーザーよりもよく知っています。コードのプロファイリングを行っていない限り、register キーワードを使用しないでください。

于 2009-04-29T23:50:02.250 に答える
3

あらゆる種類のデータ構造をループしている場合、キャッシュ ミスは、進行方向よりもはるかに大きな影響を与えます。些細なマイクロ最適化ではなく、メモリ レイアウトとアルゴリズム構造の全体像に関心を持ってください。

于 2009-04-29T23:51:45.197 に答える
2

以下を試すことができます。どのコンパイラが非常に効率的に最適化します:

#define for_range(_type, _param, _A1, _B1) \
    for (_type _param = _A1, _finish = _B1,\
    _step = static_cast<_type>(2*(((int)_finish)>(int)_param)-1),\
    _stop = static_cast<_type>(((int)_finish)+(int)_step); _param != _stop; \
_param = static_cast<_type>(((int)_param)+(int)_step))

これで使用できます:

for_range (unsigned, i, 10,0)
{
    cout << "backwards i: " << i << endl;
}

for_range (char, c, 'z','a')
{
    cout << c << endl;
}

enum Count { zero, one, two, three }; 

for_range (Count, c, three, zero)
{
    cout << "backwards: " << c << endl;
}

任意の方向に繰り返すことができます:

for_range (Count, c, zero, three)
{
    cout << "forward: " << c << endl;
}

ループ

for_range (unsigned,i,b,a)
{
   // body of the loop
}

次のコードが生成されます。

 mov esi,b
L1:
;    body of the loop
   dec esi
   cmp esi,a-1
   jne L1 
于 2011-12-09T22:14:02.460 に答える
1

カウンターを増やしているか減らしているかよりもはるかに重要なことは、メモリを上げているか下げているかです。ほとんどのキャッシュは、ダウン メモリではなくアップ メモリ用に最適化されています。メモリ アクセス時間は、今日のほとんどのプログラムが直面しているボトルネックであるため、メモリを増やすようにプログラムを変更すると、カウンタをゼロ以外の値と比較する必要がある場合でも、パフォーマンスが向上する可能性があります。一部のプログラムでは、コードを変更してメモリを下げるのではなく、メモリを上げるように変更したところ、パフォーマンスが大幅に向上しました。

懐疑的?私が得た出力は次のとおりです。

sum up   = 705046256
sum down = 705046256
Ave. Up Memory   = 4839 mus
Ave. Down Memory =  5552 mus
sum up   = inf
sum down = inf
Ave. Up Memory   = 18638 mus
Ave. Down Memory =  19053 mus

このプログラムの実行から:

#include <chrono>
#include <iostream>
#include <random>
#include <vector>

template<class Iterator, typename T>
void FillWithRandomNumbers(Iterator start, Iterator one_past_end, T a, T b) {
  std::random_device rnd_device;
  std::mt19937 generator(rnd_device());
  std::uniform_int_distribution<T> dist(a, b);
  for (auto it = start; it != one_past_end; it++)
    *it = dist(generator);
  return ;
}

template<class Iterator>
void FillWithRandomNumbers(Iterator start, Iterator one_past_end, double a, double b) {
  std::random_device rnd_device;
  std::mt19937_64 generator(rnd_device());
  std::uniform_real_distribution<double> dist(a, b);
  for (auto it = start; it != one_past_end; it++)
    *it = dist(generator);
  return ;
}

template<class RAI, class T>
inline void sum_abs_up(RAI first, RAI one_past_last, T &total) {
  T sum = 0;
  auto it = first;
  do {
    sum += *it;
    it++;
  } while (it != one_past_last);
  total += sum;
}

template<class RAI, class T>
inline void sum_abs_down(RAI first, RAI one_past_last, T &total) {
  T sum = 0;
  auto it = one_past_last;
  do {
    it--;
    sum += *it;
  } while (it != first);
  total += sum;
}

template<class T> std::chrono::nanoseconds TimeDown(
                      std::vector<T> &vec, const std::vector<T> &vec_original,
                      std::size_t num_repititions, T &running_sum) {
  std::chrono::nanoseconds total{0};
  for (std::size_t i = 0; i < num_repititions; i++) {
    auto start_time = std::chrono::high_resolution_clock::now();
    sum_abs_down(vec.begin(), vec.end(), running_sum);
    total += std::chrono::high_resolution_clock::now() - start_time;
    vec = vec_original;
  }
  return total;
}

template<class T> std::chrono::nanoseconds TimeUp(
                      std::vector<T> &vec, const std::vector<T> &vec_original,
                      std::size_t num_repititions, T &running_sum) {
  std::chrono::nanoseconds total{0};
  for (std::size_t i = 0; i < num_repititions; i++) {
    auto start_time = std::chrono::high_resolution_clock::now();
    sum_abs_up(vec.begin(), vec.end(), running_sum);
    total += std::chrono::high_resolution_clock::now() - start_time;
    vec = vec_original;
  }
  return total;
}

int main() {
  std::size_t num_repititions = 1 << 10;
  {
  typedef int ValueType;
  auto lower = std::numeric_limits<ValueType>::min();
  auto upper = std::numeric_limits<ValueType>::max();
  std::vector<ValueType> vec(1 << 24);

  FillWithRandomNumbers(vec.begin(), vec.end(), lower, upper);
  const auto vec_original = vec;
  ValueType sum_up = 0, sum_down = 0;

  auto time_up = TimeUp(vec, vec_original, num_repititions, sum_up).count();
  auto time_down = TimeDown(vec, vec_original, num_repititions, sum_down).count();
  std::cout << "sum up   = " << sum_up   << '\n';
  std::cout << "sum down = " << sum_down << '\n';
  std::cout << "Ave. Up Memory   = " << time_up/(num_repititions * 1000) << " mus\n";
  std::cout << "Ave. Down Memory =  "<< time_down/(num_repititions * 1000) << " mus"
            << std::endl;
  }
  {
  typedef double ValueType;
  auto lower = std::numeric_limits<ValueType>::min();
  auto upper = std::numeric_limits<ValueType>::max();
  std::vector<ValueType> vec(1 << 24);

  FillWithRandomNumbers(vec.begin(), vec.end(), lower, upper);
  const auto vec_original = vec;
  ValueType sum_up = 0, sum_down = 0;

  auto time_up = TimeUp(vec, vec_original, num_repititions, sum_up).count();
  auto time_down = TimeDown(vec, vec_original, num_repititions, sum_down).count();
  std::cout << "sum up   = " << sum_up   << '\n';
  std::cout << "sum down = " << sum_down << '\n';
  std::cout << "Ave. Up Memory   = " << time_up/(num_repititions * 1000) << " mus\n";
  std::cout << "Ave. Down Memory =  "<< time_down/(num_repititions * 1000) << " mus"
            << std::endl;
  }
  return 0;
}

どちらも同じことsum_abs_upsum_abs_down行い、同じようにタイミングが取られますが、唯一の違いは、sum_abs_upメモリが上がる一方でメモリsum_abs_downが下がることです。vec両方の関数が同じメモリ位置にアクセスできるように、参照渡しもしています。それにもかかわらず、sum_abs_upは一貫して より高速ですsum_abs_down。自分で実行してみてください (g++ -O3 でコンパイルしました)。

参考までに、これらの変更が将来のタイミングに影響を与えないようにしながら、変更をvec_original容易にし、それらを変更できるようにするために、実験のためにそこにいます.sum_abs_upsum_abs_downvec

私がタイミングを計っているループがどれほどタイトであるかに注意することが重要です。ループの本体が大きい場合、ループの本体の実行にかかる時間が完全に支配される可能性が高いため、反復子がメモリを上下するかどうかは問題になりません。また、いくつかのまれなループでは、メモリを下に移動する方が上に移動するよりも速い場合があることに注意してください。しかし、そのようなループであっても、上昇が下降よりも常に遅いというケースはめったにありません (メモリを上昇するループとは異なり、メモリを上昇するループは、同等のダウン メモリ ループよりも常に高速です。ほんの一握りの場合、それらは 40 でさえありました)。 +% 速い)。

ポイントは、経験則として、オプションがある場合、ループの本体が小さく、ループがメモリを下に移動するのではなくメモリを上に移動することにほとんど違いがない場合は、メモリを上に移動する必要があるということです。

于 2017-04-27T21:35:44.513 に答える