ついつい関数が長くなってしまうケース
高木です。おはようございます。
C言語に限った話ではないのですが、関数(メソッドでもいいです)がどんどん長くなってしまうのはよくありませんね。
私もそのことは重々承知しているんです。
けれども、ついつい長くなってしまうことがあります。
今日はその件についての話題です。
取り組んでいる内容次第では、関数の内容が複雑になってしまうのはしかたがありません。
普通なら、ある程度関数を小分けにして見通しをよくするなどします。
しかし、それができないケースがあるのです。
今、関わっているプラットフォームでは、スタックサイズが256バイトしかありません。
256kバイトではありませんよ。
単なる256バイトです。
32ビットマイコンなので、int型のサイズもポインタのサイズも32ビットあります。
なので、仮引数を積むのも、自動オブジェクトを割り付けるのも、原則として4バイト単位でかかっています。
関数を呼び出せば、戻り先アドレスに4バイト、フレームポインタの退避に4バイトかかります。
仮引数や自動オブジェクトをレジスタに割り付けるといったことをコンパイラはやってはくれるでしょう。
けれども、関数からさらに別の関数を呼び出せば、レジスタ変数もいったんはスタックに退避せざるを得ません。
仮引数が2つの関数を呼び出せば、行って帰ってくるだけで、16バイトもスタックを使ってしまいます。
行った先の関数内では、自動オブジェクトも使いますし一時オブジェクトも生成されます。
実際にはもっと多くのスタックを使うことになるはずです。
単に処理することが多いだけであれば、小分けにしてもスタックが足りなくなることはないと思います。
けれども、複雑な演算アルゴリズムを実現するには、結構な数の変数が必要になるのです。
その分だけスタックを消費してしまいます。
C言語といっても、使っているのはC99です。
「だったらインライン関数が使えるじゃないか」と考えるかもしれません。
しかし、C++のインライン関数とは違って、C99のインライン関数はインライン置換を示唆するためのものではありません。
ISO/IEC 9899:1999の6.7.4 Function specifiersでは次のように記述されています。
Making a function an inline function suggests that calls to the function be as fast as possible. The extent to which such suggestions are effective is implementation-defined.
つまり、インライン置換を示唆するためのものではないのです。
具体的には、ベクタージャンプ専用のインストラクションを持つプロセッサであれば、ベクタージャンプでもいいわけです。
あるいは、相対アドレッシングで分岐させることもあるかもしれません。
こんなのだと、何ひとつスタックの節約にはつながらないのです。
また、確かにインライン置換されたとしても、インライン関数に入った時点でフレームポインタを退避するような処理系も実在します。
この場合には、戻り先番地の1ワード分しか節約できないでしょう。
インライン関数ではなくマクロにするという手もあるでしょう。
しかし、極めて簡単なものならともかく、そうでなければマクロにすれば見通しがよくなるどころか、著しく可読性を低下させてしまいます。
このような事情から、ついつい関数が長くなってしまいがちになるのです。
スタックが足りなくなると、まったく動かなくなったり、メモリ破壊を起こしながら変な動きをしたりします。
それだけは避けたいところです。
なので、スタックの節約は必ずしも悪名高い時期尚早な最適化とはいえないのです。
8ビットや16ビットのマイコンの場合、スタックが256バイトどころか、RAM全体が256バイトであったり、もっと小さかったりすることもあります。
int型やポインタ型が16ビットだったりするのが救いですが、それでもかなりきついですね。
それに比べれば、今の状況はずっとマシだといえるでしょう。
制約が多ければ多いほど、プログラマーとしては腕の見せどころでもあります。
きついのはきついですが、むしろその状況を楽しんでいる自分があるのも事実です。
こういうのが楽しめないと、プログラマーという仕事はきついでしょうねえ。