26 Oct 2013, 12:54

組込み開発の二大迷信に挑む!リファクタリングにおけるパフォーマンスとスタックオーバーフローについての数値実験

組込み開発において、リファクタリングしようとすると、自分は怒られる。

  • パフォーマンスが低下する
  • スタックオーバーフローする

怒られるのが嫌で、リファクタリングできない。この2大迷信について、簡単な実験してみた。

Normalコード(Test1)

これは普通のコード。ここを参考にした。clock

[tsu-nera]% cat timer.c
#include <time.h>
#include <stdio.h>

int main(void)
{
  clock_t start, end;
  long l;
  long i=0;
  int  n=0;
  clock_t total_start, total_end;

  total_start = clock();

  while(n < 5){

    start = clock();
    i = 0;

    for (l=0; l<100000000; l++) {
      i++;
    }

    end = clock();
    printf("ループ1億回の時間: %f秒\n", (double)(end - start) / CLOCKS_PER_SEC);
    n++;
  }

  total_end = clock();

  printf("平均の時間: %f秒\n", (double)(total_end - total_start) / CLOCKS_PER_SEC / n);

  return 0;

}

Functionコード(Test2)

つづいて、関数でインクリメントを抽出したコード。

[tsu-nera]% cat timer2.c
#include <time.h>
#include <stdio.h>


void incriment(long* i) {
  (*i)++;
}

int main(void)
{
  clock_t start, end;
  long l;
  long i=0;
  int  n=0;
  clock_t total_start, total_end;

  total_start = clock();

  while(n < 5){

    start = clock();
    i = 0;

    for (l=0; l<100000000; l++) {
      incriment(&i);
    }

    end = clock();
    printf("ループ1億回の時間: %f秒\n", (double)(end - start) / CLOCKS_PER_SEC);
    n++;
  }

  total_end = clock();

  printf("平均の時間: %f秒\n", (double)(total_end - total_start) / CLOCKS_PER_SEC / n);

  return 0;

}

実験結果

気持ち、Test1のほうが早い気がする。もっと実験すれば、大数の法則で正確な値がでそうだけど、まあいいや。とりあえず、気持ちの問題で小さな関数はパフォーマンスをあまり低下させないという自身がついた。

Test1

[tsu-nera]% ./a.out
ループ1億回の時間: 1.050000秒
ループ1億回の時間: 0.720000秒
ループ1億回の時間: 0.670000秒
ループ1億回の時間: 1.050000秒
ループ1億回の時間: 1.170000秒
平均の時間: 0.932000秒
[tsu-nera]% ./a.out
ループ1億回の時間: 1.030000秒
ループ1億回の時間: 0.960000秒
ループ1億回の時間: 0.960000秒
ループ1億回の時間: 0.770000秒
ループ1億回の時間: 0.680000秒
平均の時間: 0.882000秒
[tsu-nera]% ./a.out
ループ1億回の時間: 1.250000秒
ループ1億回の時間: 0.840000秒
ループ1億回の時間: 0.670000秒
ループ1億回の時間: 0.570000秒
ループ1億回の時間: 0.630000秒
平均の時間: 0.792000秒
[tsu-nera]% ./a.out
ループ1億回の時間: 1.030000秒
ループ1億回の時間: 0.730000秒
ループ1億回の時間: 0.630000秒
ループ1億回の時間: 0.520000秒
ループ1億回の時間: 0.460000秒
平均の時間: 0.674000秒
[tsu-nera]% ./a.out
ループ1億回の時間: 0.940000秒
ループ1億回の時間: 0.720000秒
ループ1億回の時間: 0.630000秒
ループ1億回の時間: 0.540000秒
ループ1億回の時間: 0.500000秒
平均の時間: 0.666000秒

Test2

[tsu-nera]% ./a.out
ループ1億回の時間: 1.010000秒
ループ1億回の時間: 0.810000秒
ループ1億回の時間: 0.650000秒
ループ1億回の時間: 0.560000秒
ループ1億回の時間: 0.480000秒
平均の時間: 0.702000秒
[tsu-nera]% ./a.out
ループ1億回の時間: 1.470000秒
ループ1億回の時間: 1.160000秒
ループ1億回の時間: 0.930000秒
ループ1億回の時間: 0.700000秒
ループ1億回の時間: 0.590000秒
平均の時間: 0.970000秒
[tsu-nera]% ./a.out
ループ1億回の時間: 1.050000秒
ループ1億回の時間: 0.870000秒
ループ1億回の時間: 0.710000秒
ループ1億回の時間: 0.770000秒
ループ1億回の時間: 1.270000秒
平均の時間: 0.934000秒
[tsu-nera]% ./a.out
ループ1億回の時間: 1.200000秒
ループ1億回の時間: 0.890000秒
ループ1億回の時間: 0.710000秒
ループ1億回の時間: 0.650000秒
ループ1億回の時間: 0.540000秒
平均の時間: 0.798000秒
[tsu-nera]% ./a.out
ループ1億回の時間: 1.020000秒
ループ1億回の時間: 0.760000秒
ループ1億回の時間: 0.620000秒
ループ1億回の時間: 0.550000秒
ループ1億回の時間: 0.560000秒
平均の時間: 0.704000秒

スタックオーバーフローのテスト(Test3)

関数を呼びすぎるとスタックオーバーフローするよと脅されたが、脅した人はどのくらいの確信をもって発言したのかをテストした。

こんなサンプル。

#include <stdio.h>

void incriment(long* i) {
  (*i)++;
  printf("i=%d\n",*i);
  incriment(i);
}

int main(void)
{
  long  i=0;
  incriment(&i);
  return 0;

}

テスト結果

大体、自分のCygwin環境だと400000くらいでクラッシュする。

i=392880
i=392881
i=392882
[1]    18958 segmentation fault  ./a.out

ただ、スタックオーバーフローは、タスクサイズと環境依存なので、一概に安心はできないな。3階層くらいならば、へのようなものか(・・?

結論

関数抽出しても、コンパイラが最適化してくれるため、パフォーマンスは気にしない。小さな関数はコンパイラがinlineしてくれる。20年前の常識は、現代の非常識。

ただし、スタックオーバーフローは注意を払う。

21 Oct 2012, 13:25

Eclipse CDT リファクタリング機能について調べてみた。

偶然、以下のリンクの動画を見つけて、ものすごくカッコ良かった。

実践!リファクタリング – WEB+DB PRESS Vol.37

ほぼパクリだけれども、自分でもEclipse CDT版で紹介動画を作ってみた。
ソースとかは思いつきなので、参考にならないけれども。

[http://www.youtube.com/embed/huxHVMcMfR0]

 

Eclipse CDTのリファクタリング機能はJDTに比べてショボかった

Eclipse CDTのリファクタリング機能はJDTに比べて、思ったよりもショボかった。

  • インライン化の機能はない。
  • 関数の抽出でも、重複コードを見つけてくれない。
  • 関数の移動ができない

。。などなど。

できることは、以下の通り。

  • 名前変更
  • ローカル変数の抽出
  • 定数の抽出
  • 関数の抽出
  • Toggle Function

開発元のサイト(http://r2.ifs.hsr.ch/cdtrefactoring)をのぞくと、今後機能追加がされそうな雰囲気。

CDT リファクタリング機能の簡単な紹介

リファクタリング機能の簡単な説明。

実際にC言語でどのようなリファクタリングが有効かというのは、以下のサイトが非常に参考になった。リファクタリングの経典、マーチン・ファウラーの『リファクタリング』の中で、C言語で使えるものだけを選択して紹介している。

Refactoring C-code

名前変更(Alt + Shift + R)

同一ファイル名の変数名や関数名を変更できる。
個人的には、とてもよく使う。

ただし、ファイルをまたいで置換は失敗する。
この場合は、検索&置換機能で変更したほうがよい。

ローカル変数の抽出(Alt +Shift + L)

選択した部分をローカル変数に抽出することができる。
長い条件文を簡単にするときなどや、わかりにくい式に簡単な名前を与えるときに使用する。

実行すると、選択した行のすぐ上に、型つきで変数の定義と代入文があらわれる。

int sample = ( 選択した式)
if(sample(選択した式) {
}

個人的には、この逆の変数のインライン化とセットでできればいいのにと思う。

定数の抽出( Alt + C)

数字や文字列など、むき出しな状態でコードに書いてあるときは、定数の抽出を使う。
実行すると、ファイルの先頭に、定数が 『static const 』つきで宣言される。

関数の抽出( Alt + Shift  + M)

関数の抽出もよく使う。

関数として抽出したい部分を選択して実行すると、関数の頭に選択した部分を含む関数が生成される。選択した部分にreturn文が含まれると動作しないので注意が必要。また、選択した部分に含まれる変数を自動的に検出して、仮引数とする。

これはJDTのように重複する部分を自動で検出して置換してはくれないのが悲しい。

Toggle Function( Alt + Shift + T)

指定された関数をソースファイルからヘッダファイルへ移動する。
関数をインライン関数に変更したい場合に使う機能。

参考リンク