コードサイズ
コード サイズに関しては、最初の実装をファイルf1.c
に、2 番目の実装を に配置しましたf2.c
。
$ gcc -c f1.c f2.c
$ size f1.o f2.o
__TEXT __DATA __OBJC others dec hex
123 0 0 0 123 7b f1.o
119 0 0 0 119 77 f2.o
$ gcc -O3 -c f1.c f2.c
$ size f1.o f2.o
__TEXT __DATA __OBJC others dec hex
372 0 0 0 372 174 f1.o
362 0 0 0 362 16a f2.o
$
どちらの実装でも、コード サイズにほとんど違いがないことに注意してください。ただし、興味深いことに、最適化されたコードは、最適化されていないコードよりもはるかに大きくなります (約 3 倍)。
(コンパイラ: Mac OS X 10.7.5 上の GCC 4.7.1。)
階乗 (これらの関数が実装するもの) は非常に急速に成長することに注意してください。実は13!は大きすぎて 32 ビットの符号なし整数 21 に収まりません! は大きすぎて 64 ビットの符号なし整数に収まらず、35! は大きすぎて 128 ビットの符号なし整数に収まりません (そのようなタイプのコンピューターが見つかれば)。
コード速度 — 逆張りの発見!
また、仮定に注意してください。反復ソリューションは再帰ソリューションよりも高速であると予想していました。しかし、測定はそうではないことを示唆しています。
テストは、2.3 GHz Intel Core i7 (および 16 GiB メモリですが、メモリはこの計算の要素ではありません) を搭載した MacBook Pro で実行されました。
測定によると、コードが最適化されている場合、再帰ソリューションは一貫して純粋な反復ソリューションよりも少し高速です。これは私の予想とはまったく逆ですが、パフォーマンス測定が必要な理由を示しています。
最適化されたコード
# iteration
# Count = 10
# Mean = 0.799869
# Variance = 0.000011
# recursion
# Count = 10
# Mean = 0.750904
# Variance = 0.000014
後でルックアップ テーブル関数を追加しましたが、その時間は次のとおりです。
# lookuptab
# Count = 10
# Mean = 0.213836
# Variance = 0.000004
そして、テスト ハーネスのオーバーヘッドを測定するために入力パラメーターを返すだけの関数を追加したところ、次の結果が得られました。
# over-head
# Count = 10
# Mean = 0.211325
# Variance = 0.000001
したがって、配列ルックアップの計算コストは非常に小さくなります。
最適化されていないコード
オプティマイザーの能力に疑問を持ったことがある場合は、最適化されていないビルドの最適化された時間をこれらと比較してください。
# iteration
# Count = 10
# Mean = 1.852833
# Variance = 0.000020
# recursion
# Count = 10
# Mean = 2.937954
# Variance = 0.000059
ルックアップ テーブルのバージョン:
# lookuptab
# Count = 10
# Mean = 0.730275
# Variance = 0.000026
そしてオーバーヘッドバージョン:
# over-head
# Count = 10
# Mean = 0.633132
# Variance = 0.000009
一般的な観察
- 単純な「コード行」だけでは、パフォーマンスについては何もわかりません。
- 測定は当て推量に勝る。
- オプティマイザは優れています。
コードの行数を単純に数えることが適切なガイドラインではない理由は、行ごとにコストが異なるためです。たとえば、 、 、 などの関数の呼び出しを含む 1 行のコードはsin()
、cos()
(tan()
おそらく) 単一の整数算術演算と代入を含む 20 行のコードよりもはるかにコストがかかります。
問題のように、2 つの非常によく似た関数を比較すると、より複雑な再帰は単純な反復よりも遅くなる傾向があります。しかし、示されているように、特に階乗などの単純な末尾再帰関数の場合、コンパイラが最適化に成功すると、そのような推測された結果が間違っている可能性があります。
タイミングの詳細
テストプログラムは次のとおりです。
static int fun1(int num)
{
if (num == 1)
return 1;
else
return num * fun1(num - 1);
}
static int fun2(int num)
{
int i=1;
do{
i = i * num;
num--;
} while (num);
return i;
}
static int fun3(int num)
{
static const int factorial[] =
{ 1, 1, 2, 6, 24, 120, 720, 5040, 40320,
362880, 3628800, 39916800, 479001600,
};
enum { MAX_FACTORIAL_NUM = (sizeof(factorial)/sizeof(factorial[0])) };
if (num < 0 || num >= MAX_FACTORIAL_NUM)
return 0;
else
return factorial[num];
}
static int fun4(int num)
{
return num;
}
#include "timer.h"
#include <stdio.h>
static void tester(char const *name, int (*function)(int))
{
char buffer[32];
Clock clk;
unsigned long long sumfact = 0;
clk_init(&clk);
clk_start(&clk);
for (int i = 0; i < 100000000; i++)
sumfact += (*function)(i % 12 + 1);
clk_stop(&clk);
printf("%s: %s (%llu)\n", name, clk_elapsed_us(&clk, buffer, sizeof(buffer)), sumfact);
}
int main(void)
{
for (int i = 0; i < 10; i++)
{
tester("recursion", fun1);
tester("iteration", fun2);
tester("lookuptab", fun3);
tester("over-head", fun4);
}
return(0);
}
テスト コードは、2 つの関数を可能な限り対称的に扱うように注意し、各関数を交互にテストして、バックグラウンド プロセスがパフォーマンスに干渉する可能性を減らします。(通常、バックグラウンドで実行されている BOINC プロセスは、これらのテストではオフにされました。以前の質問のタイミングに関する経験から、それらが結果に深刻な影響を与え、結果により多くのばらつきをもたらすことが示されています。)
最適化された ( -O3
) ビルドの raw 時間
ルックアップ テーブルまたはオーバーヘッド関数のない初期バージョンのプログラム。
recursion: 0.754428 (4357969100681262)
iteration: 0.799330 (4357969100681262)
recursion: 0.749773 (4357969100681262)
iteration: 0.798897 (4357969100681262)
recursion: 0.747794 (4357969100681262)
iteration: 0.800977 (4357969100681262)
recursion: 0.748282 (4357969100681262)
iteration: 0.792708 (4357969100681262)
recursion: 0.748342 (4357969100681262)
iteration: 0.798776 (4357969100681262)
recursion: 0.748377 (4357969100681262)
iteration: 0.801641 (4357969100681262)
recursion: 0.750115 (4357969100681262)
iteration: 0.802468 (4357969100681262)
recursion: 0.750807 (4357969100681262)
iteration: 0.802829 (4357969100681262)
recursion: 0.751296 (4357969100681262)
iteration: 0.796841 (4357969100681262)
recursion: 0.759823 (4357969100681262)
iteration: 0.804221 (4357969100681262)
real 0m15.575s
user 0m15.556s
sys 0m0.027s
最適化されていないビルドの生時間
ルックアップ テーブルまたはオーバーヘッド関数のない初期バージョンのプログラム。
recursion: 2.951282 (4357969100681262)
iteration: 1.852239 (4357969100681262)
recursion: 2.932758 (4357969100681262)
iteration: 1.851512 (4357969100681262)
recursion: 2.924796 (4357969100681262)
iteration: 1.862686 (4357969100681262)
recursion: 2.946792 (4357969100681262)
iteration: 1.846961 (4357969100681262)
recursion: 2.941705 (4357969100681262)
iteration: 1.849099 (4357969100681262)
recursion: 2.938599 (4357969100681262)
iteration: 1.852089 (4357969100681262)
recursion: 2.930713 (4357969100681262)
iteration: 1.854765 (4357969100681262)
recursion: 2.935669 (4357969100681262)
iteration: 1.851478 (4357969100681262)
recursion: 2.938975 (4357969100681262)
iteration: 1.856979 (4357969100681262)
recursion: 2.938250 (4357969100681262)
iteration: 1.850521 (4357969100681262)
real 0m47.980s
user 0m47.939s
sys 0m0.041s
両方の階乗関数のコードにバグがあることに注意してください。0! を計算するように求められると、どちらも長時間実行ループに入ります (そして、32 ビットint
型をオーバーフローさせることによってあらゆる種類の未定義の動作を呼び出します)。これは実際には適切に定義されており、値は 1 です。そのため、テスト ハーネスでの呼び出しは私が最初に書いたよう(*function)(i % 12 + 1)
ではなく。(*function)(i % 13)