PC用のアプリケーションを作るだけなら、メモリの安くなった現在ではほとんど気にすることのないメモリ効率。でも特定の目的や開発環境ではかなり、いやとっても重要なこと。
メモリ(ROM/RAM)の無駄遣いを減らすためのコードの書き方。
おしながき
処理速度との関係.
保守性 | 処理速度 | メモリ効率 | 難易度 |
処理速度に関するテクニックと、メモリ効率向上に関するテクニックが相関を持つ場合があります。相関の状態は主に次の3つだと思います。
- 処理速度の向上とメモリ効率の向上が同時に達成できる。
主に処理の無駄を削減するものが該当します。同じ処理を行うために必要なコードが減る分、処理速度もメモリ効率も向上します。
そもそも無駄な処理を行うコードを記述していることに問題があるケースがほとんどだと思います。無駄な処理を行わないコードを書けるように、能力を向上させることが必要です。 - 処理速度の向上とメモリ効率の向上が背反する。
処理速度の向上がメモリ効率の低下を、あるいはその逆にメモリ効率の向上が処理速度の低下を招く場合があります。
性能の向上を望まれていない方を犠牲にできるものか検討が必要になるでしょう。あるいは両方がバランスするように工夫することが必要かもしれません。どのように解決するのが良いかはケースバイケースであり、ここでは結論を出せません。 - 相関がない、あるいはほとんどない。
ループ内不変式の移動のようなものが該当します。処理速度の向上を行っても、メモリ効率については変化しないなどのケースです。このような工夫は積極的に行う価値があります。
if〜elseとswitch〜case〜defaultの使い分け.
保守性 | 処理速度 | メモリ効率 | 難易度 |
課題.
条件分岐を記述する場合に、if〜else でも switch〜case〜default でもどちらでも使えることがあります。このような場合にはどちらを使うべきか悩むことになります。
改善案.
以下のような基準で判断するのが良いと思います。
- 条件式(あるいはcase文)が2以上ならswitch文を使用。
- 今は条件式(あるいはcase文)が1以下だが、将来2以上に増える可能性があるならswitch文を使用。
- その他はif文を使用。
理由.
このような判断基準を勧める理由は、switch文の方が可読性が良いというのが主な理由です。
また、処理速度やメモリ効率を考慮しても上記の判断基準が有効です。if文を使用した場合は、ソースコードのコンパイル結果は、以下のように比較命令と条件分岐命令がif文の数だけ並ぶことになります。
/* C言語のソースコード */ if (var1 == 1) { (処理1) } else if (var1 == 2) { (処理2) } |
; アセンブリ言語のコンパイル結果 cmp [var1], 1 ; この2行が if (var1 == 1) jnz value_is_not_1 ; (処理1) jmp finish value_is_not_1 cmp [var1], 2 ; この2行が if (var1 == 2) jnz finish ; (処理2) finish |
補足.
switch文を使用した場合、対応するプロセッサの命令体系によって、生成されるコードが異なります。プロセッサがcaseキーワードに続く値と分岐先をセットにしたテーブルを検索して分岐先を決める、まさにswitch〜case〜defaultそのままの機能の命令を持っているなら、その命令が使われるでしょう。
しかし現在のプロセッサの多くはそのようなswitch〜case〜defaultそのままの機能の命令などは持っていません。そのため多くのコンパイラはswitch〜case〜defaultに対して、case1つにつき1回の比較命令と条件分岐を生成し、それをcaseの数だけ繰り返すのが、一般的なようです。
条件式(あるいはcase文)が2程度以上ならif文よりswitch文を使用した方が、処理速度やメモリ効率が良いと言われているのは、さまざまな有識者の経験的なものです。
定数データのconst修飾.
保守性 | 処理速度 | メモリ効率 | 難易度 |
課題.
参照だけしか行わないテーブルなどのデータを配列変数で確保することはよくあります。その定義の仕方によりメモリを無駄に消費する場合があります。
- ローカル変数として確保した場合。
void func(void) { int table[8] = { 4, 3, 7, 8, 10, 17, 1, 0 }; /* 値は例です */
実際に確保されるものは以下のとおりです。
- スタック上に配列変数tableの実体が sizeof(int)*8 バイト。
- 定数領域(あるいはコード領域)にtableの初期値リストが sizeof(int)*8 バイト。
実体の他に初期値リストが確保されることにも注意が必要です。関数funcに処理が移ると、初期値リストから値が配列変数tableの実体にコピーされることで、配列変数tableの初期化が行われます。
- 静的変数として確保した場合。
int GlobalTable[8] = { 4, 3, 7, 8, 10, 17, 1, 0 }; void func(void) { static int table[8] = { 4, 3, 7, 8, 10, 17, 1, 0 };
この場合、実際に確保されるものは以下のとおりです。
- データ領域に配列変数GlobalTable, tableの実体がそれぞれ sizeof(int)*8 バイト。
- 定数領域(あるいはコード領域)にGlobalTable, tableの初期値リストがそれぞれ sizeof(int)*8 バイト。
上記のローカル変数と同様に、実体の他に初期値リストが確保されます。ただし初期化のタイミングはプログラムの実行開始時に1回だけ、初期値リストから値が配列変数GlobalTable, tableの実体にコピーされるだけです。
どちらの場合にも、1つの変数の定義に対して、変数の実体と初期値リストの2つが確保されることになります。本節で前提としているのは、変数の参照だけ行い値を変更することはない場合です。値を変更しないのに、値の変更が可能なように、データ領域に実体を確保しているのは無駄です。
改善案.
静的変数の場合は、変数の定義にconst修飾子を記述することで解決します。
const int GlobalTable[8] = { 4, 3, 7, 8, 10, 17, 1, 0 }; void func(void) { static const int table[8] = { 4, 3, 7, 8, 10, 17, 1, 0 };
このように定義すると、変数の実体は定数領域(あるいはコード領域)に配置された初期値リストのみになります。変数の値の初期化のために、初期値を実体にコピーする必要もなくなるので、コピーのコードが消費していたメモリ(コード領域)とその実行時間も削減できます。
つまり効果は、データ領域・定数領域・コード領域の削減、処理速度の若干の高速化となります。
注意.
例ではよく使用されるケースとして配列変数を示しましたが、スカラ型変数でも同様です。
最初の例のように変数をスタック上に確保されるローカル変数として定義していると、const修飾しても効果は得られません。静的に確保されるようにブロック外で定義するか、static記憶クラス指定子を併用する必要があります。
Copyright 2005-2016, yosshie.