• ベストアンサー

【C言語】再帰が時間がかかる理由について

再帰のプログラムがなぜ時間がかかるのかを詳しく調べています。 アセンブリレベルで見ると、 callに時間がかかるのか?それともpop命令?それとも退避? 結局、よく分からないまま、 Google検索して調べています。(まだよくわかってません。) 再帰呼び出しのデメリットは、 スタック領域を大量に消費する、関数呼び出しのオーバーヘッドであること。 この事実はわかりました。 しかしながら、 オーバーヘッドとは具体的に何なのか これを調べています。 どなたか、良いサイト・書籍を知らないでしょうか。 教えてください。

質問者が選んだベストアンサー

  • ベストアンサー
  • jjon-com
  • ベストアンサー率61% (1599/2592)
回答No.3

2つの要因があると思います。 -------- 1つ目は次の例のように,再帰的定義の中に自分自身が1つだけ登場する場合です。 「1からnの総和」 F(n)=n+F(n-1) …(n>1 であること) F(1)=1 F(5) =5+F(4) =5+(4+F(3)) =5+4+(3+F(2)) =5+4+3+(2+F(1)) =5+4+3+2+1 この場合,上記に示したように,結果的に行われる計算自体は次のループと変わらず,5+4+3+2+1 を行っています。 s = 0; for (i = n; i >= 1; i--) { s = s + i; } しかし,5,4,3,2,1のそれぞれを求めるために再帰では毎回関数呼び出しを用いています。 回答No.1・No.2の指摘どおり「関数呼び出しを用いること自体がオーバヘッドである」ということです。 -------- 2つ目は次の例のように,再帰的定義の中に自分自身が複数登場する場合です。 「フィボナッチ数」 F(n)=F(n-1)+F(n-2) …(n≧2 であること) F(1)=1 F(0)=0 F(5) =F(4)+F(3) =(F(3)+F(2))+(F(2)+F(1)) ={(F(2)+F(1))+(F(1)+F(0))}+{(F(1)+F(0))+1} =[{(F(1)+F(0))+1}+(1+0)]+{(1+0)+1} =[{(1+0)+1}+(1+0)]+{(1+0)+1} =5 フィボナッチ数は,0, 1, 1, 2, 3, 5, 8, 13, 21, 44, ……のように列挙できる,前2者の和です。 この再帰の場合,前述の「関数呼び出し自体がオーバヘッドである」ことはもちろんなのですが,加えて「関数呼び出しの回数が増えていること」が新たなオーバヘッドとなります。 具体的には,次の★で示したF(2)を求める箇所(ちなみに,F(2)=F(1)+F(0)=1+0=1です)は3つ登場しますが, F(5) =F(4)+F(3) =(F(3)+F(2)★)+(F(2)★+F(1)) ={(F(2)★+F(1))+(F(1)+F(0))}+{(F(1)+F(0))+1} ある★箇所で求めたF(2)の値を,他の★箇所で流用することは再帰では行われません。式が2つの項に分かれ,4つの項に分かれ,と分割されていきますが,ある一つの項で値を求める際,すでに他の項でその値が算出済であるかどうか知りませんし,自分が求めた値をそれを必要とするであろう他の項に教えようともしません。 再帰的定義の中に自分自身が複数登場する場合は,関数呼び出しの回数がネズミ算的に増えていきます。

その他の回答 (2)

  • chie65535
  • ベストアンサー率43% (8522/19371)
回答No.2

再帰コールってのは、要は「現状の自分を保護したまま、自分自身を呼んで、帰って来た時に、呼ぶ前の状態にちゃんと戻っている」って必要がある。 この「自身の保護と、呼び出し前の状態への回復」が、オーバーヘッドになる訳。 再帰コールでは、特定の目的を除いては、グローバル変数は使えない。 「自分から帰って来た時」に、もし、グローバル変数の値が変わっていたら、「呼ぶ前と呼んだあとで、値が違う」って事になって「呼び出し前の状態への回復」が出来なくなる。 もし、ローカル変数を100個使っているなら、ローカル変数100個分の保護と復帰が必要。 そのローカル変数が、スタック上に置かれるオート変数なら、保護と復帰はスタックを操作するだけで済むけど、もし、スタック上に置けないなら「スタックや別のメモリ空間への退避と復帰」が必要になってしまう。 場合によっちゃ、1回再帰コールするだけで、数千バイトのメモリを転送する必要が出る場合もある。 もし再帰コールで「退避と復帰」が要らないなら、変数などはすべて「固定アドレスのメモリ」に置いておく事ができる。 例えば「最初に見付かったディレクトリがあったら、そのディレクトリでも同じ事をして、ディレクトリを連結した文字列を作る」って場合。 この場合は、自分を再帰コールしたとしてもお「自分から帰って来たら、あとは何もしない」ので、退避も復帰も不要な書き方が出来る。 退避と復帰が不要なら、再帰コールしても、スタックの消費は「戻りアドレス」だけで済むし、退避も復帰もしないから、通常の関数コールよりもオーバーヘッドは小さくなる。普通の関数コールだって、必要最低限の退避と復帰が必要になるからね。 そういう訳で、オーバーヘッドの大半は「退避と復帰」にあります。

  • zwi
  • ベストアンサー率56% (730/1282)
回答No.1

まぁ、Wikipediaなどは先に読みましょう。 「オーバーヘッド - Wikipedia」 http://ja.wikipedia.org/wiki/%E3%82%AA%E3%83%BC%E3%83%90%E3%83%BC%E3%83%98%E3%83%83%E3%83%89 で、callなのか、pushなのか、popなのかですが、それらを含めた全てです。ループで表現した時には出てこない再帰だけにあるもの全てがオーバーヘッドとなります。 それとCPUに依りますがcallで発生する命令キャッシュのフラッシュもオーバーヘッドになります。

関連するQ&A

  • VC++ 再帰呼び出しについて

    VC++6.0にてプログラミングを行っているものですが、 関数の再帰呼び出しについて質問です。 再帰呼び出しの際にスタックに積まれる変数というのは、 再帰呼び出しをする関数に渡す引数のことですか? スタックオーバーフローを起こさないために、 staticなポインタにHeap領域上の 変数を割り当てるとよい。 と分かったのですが、 この意味は、例えば static int *a = new int; ということなのですか?

  • 再帰的(リカーシブ)プログラムの説明について。

    以下は、再帰的(リカーシブ)プログラムの説明を記載しました。 この説明文でおかしい箇所の添削をお願い出来ないでしょうか? 宜しくお願い致します。 以下からになります。 再帰的(リカーシブ)プログラムとは、プログラムの中から自分自身を呼び出して実行することを再帰的(リカーシブ)アルゴリズムといい、この形式で再帰呼び出しを行うプログラムのこと。 まずは、再帰的アルゴリズムについて、例を使って説明を行いたい。 主プログラムとサブルーチンaがある。 主プログラムは、文字通り、主(メイン)となるプログラム。 サブルーチンは、主プログラムが呼び出して利用する処理をひとまとめにしたもの。 文字通り、サブとなる処理を行う。 主プログラムには、CALL aという命令が記述されている。 これはサブルーチンaを読み出すという命令。 この再帰的プログラムは、処理が終わったら、読み出された場所に帰っていく。 このため、戻り場所を記憶しておかないと帰る事が出来ない。 この戻り場所を記憶するのが、LIFO方式による記憶領域になる。 LIFO方式の記憶領域だから、スタック領域になる。 スタック領域だから、後入れ先出しで戻り場所を記憶していく。 まずは、1回目の呼び出しとして、主プログラムがサブルーチンaを呼び出している。 1回目の戻り場所を記憶しておく。 次にサブルーチンaを見ると、CALL a、つまり自分自身を読み出している。 これが2回目の読み出し。 このように自分自身を呼び出すことを再帰呼び出しという。 同じプログラムの中で自分自身を読み出しているのだが、コンピューターは、あたかも別のサブルーチンがあるように処理が行われている。 この場合、それぞれの処理で、別の変数を用意しながら処理を行う。 このサブルーチンで処理が終わった場合にも、もとに戻る必要がある。 これは2回目の呼び出しになるため、2回目の戻り場所を記憶しておく。 更に、3回目として再びサブルーチンaを呼び出す。 3回目の戻り場所を記憶し、また別の変数を用意しながら処理を行う。 ここで最後のサブルーチンで処理が終わったとする。 処理が終わったら、呼び出された場所に戻る。 戻り場所の記憶を見てみると、上から戻る順番に記録されていることがわかる。 戻り場所はLIFO方式、後入先出しで記録されているから、最後に呼び出した3回目の戻り場所が1番上に記録され、次に2、最後に1が記録されている。 最初は戻り場所を記憶した記憶領域を参照して、3回目に呼び出された場所に戻る。 ここで3の戻り場所が消える。 そして引き続き処理が行われる。 次に、2回目に呼び出した処理が終わり、2回目に呼び出された場所に戻り、2の戻り場所が消える。 また引き続き処理が行われ、1回目に呼び出した処理が終わり、1回目に呼び出された場所(主プログラム)に戻り、1の戻り場所が消える。 そして処理が行われ、プログラム全体が終了する。 このように、プログラムの中で自分自身を呼び出し、戻り場所を記憶しながら実行するようなプログラムを再帰的(リカーシブ)プログラムという。

  • 再帰呼び出しになってしまうのをループの形にしたい

    VBで繰り返して実行するプログラムを作ったのですが、 スタック領域をオーバーしてしまいます。 再帰呼び出しになっているのはわかったのですが、 改善ができません。 簡略したら下記のような状態でした。 Sub test1()  test2 End Sub Sub test2()  test1 End Sub このtest1を実行して、繰り返し作動するようにしたのですが、 当然スタックオーバーフローになります。 簡単にループに変形できるのでしょうか?

  • メモリーにも「スタック」という領域はあるの?

    「コンピュータはなぜ動くのか 知っておきたいハードウエア&ソフトウエアの基礎知識」書籍内のP61のところにある「表3.1 Z80 CPUの主な命令」の表の「メモリーとCPUの入出力命令」の欄に 「PUSH reg:regをスタックに書き込む」 「POP reg:スタックからregに読みだす」 というのがあるのですが、これはつまりメモリーにも「スタック」領域はあるということなんでしょうか? 「スタック」と聞くと、CPUのレジスタ(記憶する箇所)のことをイメージしてしまい、メモリにはそんな領域はないと思われますが、メモリーにも「スタック」領域はあるのでしょうか? わかりやすく教えてください。 よろしくお願いいたします。

  • 深さ優先探索(再帰なし&あり)

    深さ優先探索で再帰呼び出しを用いないのと用いるプログラムを書く課題がありまして、まだC言語かけだしの自分にはあまりよくわかりません・・・ どこかにわかりやすいサイトとかってありますでしょうか?アルゴリズムとデータ構造という授業をとっているんですが、教授が妙な関西弁でしかも教え方も上手とはいえない感じなので全然理解が深まらず困っています。周りも140人中50人くらい単位落としてるカンジなんです。周りは「あんなんんじゃ取れない」といってあきらめていってるのが大半なんですが自分は興味もありますし、1度落としているだけに今年は絶対単位を取りたいと意気込んでます。 オーダー、マージソート、リスト、スタック、深さ優先探索などアルゴリズム(Cのプログラムでよく書かされます)についてわかりやすいサイトや書籍がありましたら是非おしえてください。

  • 機械語とアセンブリ言語について。

    (機械語データ) (アセンブリ言語) b8 57 61 6b 61 mov $0x616b6157,%eax 53 push %ebx 50 push %eax ba 04 00 00 00 mov $0x4,%edx bb 01 00 00 00 mov $0x1,%ebx b8 04 00 00 00 mov $0x4,%eax 89 e1 mov %esp,%ecx cd 80 int $0x80 58 pop %eax 31 c0 xor %eax,%eax 5b pop %ebx c3 ret こちらのアセンブリ言語の命令がわかるおすすめの書籍を知らないでしょうか? 教えていただけないでしょうか?すみません。

  • システムコールについて。

    アセンブリ言語で、int $0x80はシステムコールと言われていますが、Linuxを知らないとわかりません。システムコールはアプリとカーネルのインターフェースです。 x86 Linux 32bitのシステムコールの呼び出しは int 0x80です。 システムコールはEAXに格納されている数値でいろいろな処理ができます。 https://www.mztn.org/lxasm64/x86_x64_table.html を見ていただくとWRITEのsyscall#は4です。 mov $0x4,%eax でeaxに4を入れているので画面に出力したいのだとわかります。 WRITEの第2引数は画面に出力したい文字列が格納されているアドレスでECXに格納します。 mov %esp,%ecx とスタックポインターのアドレスをecxに入れています。 ESPは push eax でEAXに格納されている$0x616b6157(Waka)がスタックに退避しています。 WRITEの第3引数は文字数です。文字数はEDXに格納します。 mov $0x4,%edx と4が入っているので文字数は4です。 このプログラムを実行させると画面にWakaと表示して元の画面に戻ります。そのためのRETです。 C言語で書けばたった1行。 write(1,"Waka",4) これについて詳しく教えていただけないでしょうか?すみません。

  • ハードウェアでスタック構造をサポート2

    (c)一般的なアーキテクチャではハードウェアでスタック構造をサポートしているものが多いが、その機構を説明せよ。 スタック操作という手法がある。スタック操作は,スタック・ポインタで操作するアドレスを保持し,スタックへ書き込み(押し込み)したデータ数だけポインタ値を調整する手法のこと.スタックへのデータ格納命令(PUSH,CALLなど)を実行すると,スタック・ポインタが保持するアドレスは格納データ数だけ若くなり,逆にデータ取り出し命令(POP,RETなど)を実行するとそのデータ数だけポインタ値は大きくなる。 (d)前問で説明したストック構造の代表的な使い方を説明せよ。 アドレス修飾レジスタの中でスタックポインタ(SP)を持つものがある。スタックポインタはアドレスレジスタの一種で、コールスタックの先頭を指すポインタレジスタである。これが示すアドレスの内容を読み出すと同時にアドレスを増やす、逆に、示すアドレスに書き込むと同時にアドレスを減らす、といった動作を行えるものが多い。 というふうにまとめてみました。ご確認お願い致します。  

  • アセンブリ言語の質問です

    「 100人分の試験点数がある。 一人分のデータは32bit 符号無し整数でそれが連続して格納されている.先頭のアドレスがEAXで与えられる時,全員の点数の合計をEAXに入れて戻るようなサブルーチンをアセンブリ言語で書きなさい。 • loop unrollingを使用して,ループ内容を4倍に展開して,条件分岐数を減らすこと • 他のレジスタの値は保存すること • 合計点はEAXレジスタに十分納まるものとする。 • 次のような命令を使ってよい。 ADD EAX, [EBX] 」 というような問題が出て自分で解答を作ってみたのですがこれでよいのでしょうか?詳しい方ご検討よろしくお願いいたします。 PUSH EBX(EBXをスタックにおいておく) PUSH ECX(ECXをスタックにおいておく) MOV ECX 25(ECXに25を代入。4回の操作を25回すれば100回になるからである。) label0:ADD EAX [EAX] ([EAX]をEAXに加算) ADD EAX [EAX+1]([EAX+1]をEAXに加算) ADD EAX [EAX+2]([EAX+2]をEAXに加算) ADD EAX [EAX+3]([EAX+3]をEAXに加算) MOV [EAX] [EAX+4]([EAX]を[EAX+4]に移動させる) EBX=EBX+1 (EBXはこのループを何回やったか、という数) CMP ECX EBX(25とEBXを比べる) JNZ:label0(比べてEBXが25になってないならば繰り返す。25になったら終了。) POP EBX(EBXをスタックから戻す) POP ECX(EBXをスタックから戻す)

  • C言語 再帰的

    失礼します。現在指定したディレクトリのファイルを取得したいのですがどういった関数を使えばいいかわからず手の付け方がわかりません。ヒントをいただけると嬉しいです。 例ですがTestディレクトリのSelect.csv,Select1.csv,,Select2.csv,を読み込み その後Test2ディレクトリの Select3.csv,Select4.csv,,Select5.csv,を読み込みたいです。 ディレクトリ構造 Test------------------------------------------------------------------------------Select.csv                           |----Select1.csv               |----Select2.csv                            |----test.text Test2-------------------------------------------------------------Select3.csv |----Select4.csv |----Select5.csv text.json hoge.text

専門家に質問してみよう