名前がおかしい。

extern int checkStatus(void);
static int status;

int checkStatus() {
  status = !status;
  return status == 0;
}

間違っている点は

  • 宣言と定義の返り値が違う。enum型の大きさがintとは限らない。
  • 宣言と定義の仮引数リストが違う。
  • check〜という名前にも関わらず値を更新している。値を更新するならupdate〜など、変更することを明示した名前にするべき。
  • 値を更新するなら検査値を返すべきではない。Query/Commandの分離が必要。
  • 通例、int型の検査値を用いる場合はエラーコード、つまり正常時に0、を返す。

などなど。

「経済学と似ている」というのは未科学ということだからね。

http://www.symmetric.co.jp/hiyama/SoftwareEngineering.htmの話。http://d.hatena.ne.jp/kilrey/20090226#p1とも関連する。

大事なことなので二度引用する。

僕自身は、ソフトウェア工学を、「どうでもいい(したがって、あまり役に 立たない)ソフトウェア工学」と「まっとうな(役に立つと期待できる)ソフトウェア工学」の2種類に分けて考えています。アジャイル派の人々から目の カタキにされているソフトウェア工学は、どうでもいいほうのソフトウェア工学ですね。実際、「ソフトウェア工学」と冠した書籍や記事のなかには、しょうもないことを述べているものもあります。

この文章を読んだ際に「"「ソフトウェア工学」と冠した書籍や記事"の大半はしょうもないことを書いているけどなあ」と思っていた。その疑問が解けた気がする。

経路による劣化。

ソフトウェア工学」には

がある、という点までは同意できる。しかし、"「ソフトウェア工学」と冠した書籍や記事"には

に加えて

があり、大半は最後のグダグダなものなのではないだろうか。

ソフトウェア工学」は未科学

私が思いつくまっとうな「ソフトウェア工学」は

に集約される。
ここには「ソフトウェア工学」の専門家がいない。もちろん「ソフトウェア工学」の専門家の言う内容もそれほど違うわけではないのだから、他の「ソフトウェア工学」がどうでもいいものだというわけではない。しかし、UNIX哲学は工学ではない。あくまでもただの"philosophy"=指針/信念だ。誤差測定どころか再現性すらないものを工学と呼ぶのは不適当だろう。

〆。

私はアラン・ケイのOSプロジェクトこそが工学だと思う*1。あれはシステムの複雑さをソースコード量=情報量の下限値として探る実験だ。結果として得られたソースコード量は真の下限値に対する良い近似になるだろう。他のOSのソースコード量がその下限値を超えている分は

といったものなのではないだろうか。
実際のところ、「対応環境を増やすためのコンフィギュレーション」と「高速化のためのダーティ・ハック」とが結びつくとソースコード量が際限なく増える。Windowsくらいになると大半がそちらに属するのではないかとさえ思う。

ざんねん!ピカチュウはよだれをたらしなきわめいている。

http://d.hatena.ne.jp/Longsword/20091130/1259573673の話。

ほとんどの場合、イオナズン一択になってしまうドラクエとは大違いです。

消費MP0の特技がもっとダメ感を強めているなぁ。むしろ山彦イオナズンができるだけマシとすら思う。

資源分配の厳しいゲームは良いゲーム。

さて、リンク先のモデルは

目的
対戦での勝利
資源
スキルの総コスト
資源
残レベルアップ回数

とまとめられると思う。純粋スキル制と比べると同じスキル構成であっても取った順番によって能力が変わるという複雑さがある。さらにレベルアップにかかる時間に応じて、試行回数に制限をかけることができる。
たぶん、この試行回数の制限が重要だ。

試行回数が少ない場合

MMORPGにありがちなのではないかと思うのだけど*1、PCを育てるのに数ヶ月かかり、それより短い周期でパッチが当たってバランスが変化するゲームを考える。
この場合、廃人でもなければ一つのバランスに対する試行回数は1, 2回止まりになる。最強のPCを計画的に育てることはできない。常人がそれなりのPCに仕上げるためにこぞって攻略サイトを参考にした結果、似たようなPCが溢れるという結果になると予想する*2

試行回数が多い場合

逆にPCを育てるのに1時間くらいしかかからないとすれば、序中盤に有効なスキルを取る必要が薄れる。育成効率が悪くなったとしても2,3時間で終わるのなら我慢できるからだ。
その結果、低レベル時に基礎ステータス上げ用スキルを取り、高レベル時に主力戦闘スキルを取る、という選択になるだろう。このスキルは最低取得レベルがNNだからレベルアップボーナスがXX稼げて……という計算が必要だが、あくまでも複雑なスキル制という範疇で考えられるものになると思う。

〆。

試行回数を多めにした方が面白そうだと思うが、最適な試行回数が幾つなのかは分からない。とりあえず、試行回数が少ない方はずるずる続ける人を狙った月額払いっぽい。
それから、大技は永続コストを払わないとならない、というルールもありだろう。

ピカチュウ!10万ボルトだ!
ここでSANチェック*3を行い、タイトルへ続く。

*1:※やったことがない。

*2:一方、廃人は局所解となるようなPCを一通り育てた。

*3:※こちらもやったことがない。小説のみ。

挿入ソートの話のまとめ

gcc4.3 -O3 on Atom, N=10000の条件で挿入ソートを測った結果(ソースコードは下に)。所要時間(time)を測っただけでは振る舞いが掴めないので、cachegrindでD1キャッシュ読み回数(D1read)を数えてみた。

関数 メモリ time D1read
yane0 malloc 9.79 62M
wkpd0 malloc 9.82 27M
yane1 static 8.09 27M
wkpd1 static 11.35 28M

大規模なソートということでmallocで確保したメモリをソートする場合、最適化がよく効いている限り、どちらもほぼ同等の速度で動くようだ。
ただし、やね版はstaticに確保したメモリをソートする場合、Wikipedia版よりも約3割速く動いた。ほぼ本人の言っている通りの成績だ。

〆。

とはいえ、jのオフセットや番兵のありなしで20%くらい揺らぐのも事実。要素数によっても要素サイズによってもCPUによってもコンパイラによっても簡単に変わる話なので、両方実装して速度を比較しないと判断できない、といったところだろうか。
あと、iccではWikipedia版の方が速い、というより、iccではやね版が妙に遅い、という傾向がある。オプション調整で速くなるのかもしれないけれど試さない。

ソース。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

void yaneurao_ins_sort_000(int data[], size_t const size) {
  int i, j, tmp;
  for (i = 1; i < size; ++i) {
    tmp = data[i];
    if (data[i-1] > tmp) {
      j = i;
      do {
        data[j] = data[j-1];
        --j;
      } while (j > 0 && data[j-1] > tmp);
      data[j] = tmp;
    }
  }
}

void wikipedia_ins_sort_000(int data[], size_t const size) {
  int i, j, tmp;
  for (i = 1; i < size; ++i) {
    tmp = data[i];
    for (j = i; j > 0 && data[j-1] > tmp; j--) {
      data[j] = data[j-1];
    }
    data[j] = tmp;
  }

}
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#define MAX 100000
static int local[MAX];

void yaneurao_ins_sort_001(int const orig[], size_t const size) {
  int i, j, tmp;
  if (MAX < size) {
    return;
  }
  memcpy(local, orig, size*sizeof(int));
  for (i = 1; i < size; ++i) {
    tmp = local[i];
    if (local[i-1] > tmp) {
      j = i;
      do {
        local[j] = local[j-1];
        --j;
      } while (j > 0 && local[j-1] > tmp);
      local[j] = tmp;
    }
  }
}

void wikipedia_ins_sort_001(int const orig[], size_t const size) {
  int i, j, tmp;
  if (MAX < size) {
    return;
  }
  memcpy(local, orig, size*sizeof(int));
  for (i = 1; i < size; ++i) {
    tmp = local[i];
    for (j = i; j > 0 && local[j-1] > tmp; j--) {
      local[j] = local[j-1];
    }
    local[j] = tmp;
  }
}
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>

extern void yaneurao_ins_sort_000(int data[], size_t const size);
extern void wikipedia_ins_sort_000(int data[], size_t const size);
extern void yaneurao_ins_sort_001(int const data[], size_t const size);
extern void wikipedia_ins_sort_001(int const data[], size_t const size);

int main (int argc, char *argv[]) {
  int i, loop, size;
  clock_t start, stop;

  if (argc < 3) {
    return -1;
  }
  loop = atoi(argv[1]);
  size = atoi(argv[2]);

  int *orig = malloc(size*sizeof(int));
  int *data = malloc(size*sizeof(int));
  
  srand(0xdeadbeef);
  for (i = 0; i < size; ++i) {
    orig[i] = rand();
  }

  start = clock();
  for (i = 0; i < loop; ++i) {
    memcpy(data, orig, size*sizeof(int));
    yaneurao_ins_sort_000(data, size);
  }
  stop = clock();
  for (i = 1; i < size; ++i) {
    if (data[i] < data[i-1]) {
      puts("ERROR!");
    }
  }
  printf("%f\n", (stop-start)*1E-6);

  start = clock();
  for (i = 0; i < loop; ++i) {
    memcpy(data, orig, size*sizeof(int)); 
    wikipedia_ins_sort_000(data, size);
  }
  stop = clock();
  for (i = 1; i < size; ++i) {
    if (data[i] < data[i-1]) {
      puts("ERROR!");
    }
  }
  printf("%f\n", (stop-start)*1E-6);

  start = clock();
  for (i = 0; i < loop; ++i) {
    yaneurao_ins_sort_001(orig, size);
  }
  stop = clock();
  printf("%f\n", (stop-start)*1E-6);

  start = clock();
  for (i = 0; i < loop; ++i) {
    wikipedia_ins_sort_001(orig, size);
  }
  stop = clock();
  printf("%f\n", (stop-start)*1E-6);

  free(data);
  free(orig);

  return 0;
}
CC     = gcc
CFLAGS = -O3
#CC     = icc
#CFLAGS = -O3

all:a.out
	./a.out 100000000 10
	./a.out 1000000 100
	./a.out 10000 1000
	./a.out 100 10000
	./a.out 1 100000

cg:a.out
	valgrind --tool=cachegrind ./a.out 1000000 10
	valgrind --tool=cachegrind ./a.out 10000 100
	valgrind --tool=cachegrind ./a.out 100 1000
	valgrind --tool=cachegrind ./a.out 1 10000
	find . -name 'cachegrind.out.*' -exec cg_annotate {} \;

clean:
	${RM} *.out *.o
	${RM} cachegrind.out.*

.c.o:
	${CC} -c ${CFLAGS} $?

a.out:extern.o sort_000.o sort_001.o
	${CC} ${CFLAGS} extern.o sort_000.o sort_001.o -o $@

挿入ソートの話の途中経過

Atom/gcc -O3だとループ変数をj=iからj=i-1に変えるといった、本質的ではない変更でも20〜30%の速度変化が起こり得るようだ。これはiccで比較してみた方が良いかもしれない。それらを含めた印象としては

  • Wikipedia版もやね版も最適化がよく効いている場合には大差ない。
  • やね版の方が最適化に失敗することが少ないようだ。

というところ。

余談。

素数の少ないソートを想定していたのか。巨大なソート済み配列への追加なら「当たり前だけど、二分検索+memmoveの方が速いよなあ」という結論で終わろうと思っていたのに。

引数の評価順序。

ありがちダメなコードのダメな理由を書くコーナー。

現象。

#include <stdlib.h>
#include <stdio.h>

int main() {
  printf("puts(\"A\"), puts(\"B\"), puts(\"C\")+puts(\"D\") = %d, %d, %d\n",
         puts("A"), puts("B"), puts("C")+puts("D"));

  return 0;
}

コンパイルして実行すると、

$ gcc -std=c99 -pedantic -O0 -g tmp.c
$ ./a.out
C
D
B
A
puts("A"), puts("B"), puts("C")+puts("D") = 2, 2, 4

と出力された。実は書いている本人も、gccの標準設定ではD C B Aの順に評価されがち、と思っていたのだけど違ったらしい。

説明。

C言語では二項演算子の左右の式や関数に与えた引数はどのような順序で評価されるかを未規程としている。未規程なのでどのような順序で評価されるかは判らないので、上の例は出力が乱れる可能性がある。とは言え言語仕様に反している訳ではないので、何らかの順序で評価されるという点で未定義とは大違いだ。
ちなみに未定義、未規程、実装依存の違いを下のようなもので、実装依存すらほとんど頼りにならないことが分かる。

未定義
どのような動作をするか一切判らない。冗談半分に「鼻から悪魔が出る」という慣用句を使うが、実際にもブルースクリーンでPCが固まるくらいは覚悟しておいた方が良い(エラーも何も出さずに間違った結果を返す方が厄介だけれど)。
未規程
大まかな動作は仕様から予想できるが、詳細については一切判らない。10000回に1回だけ評価順序を反転します、という怖いコンパイラだったとしても知ることはできない。
実装依存
大まかな動作は仕様から予想でき、詳細についてはコンパイラの文書などから知ることができる(はず)。ただし、乱数を用いて順序を決めます、という記述だったらどうするのかという疑問が残る。

副作用のタイミング

ありがちダメなコードのダメな理由を書くコーナー。

現象。

C99ソースコード(tmp.c)

#include <stdlib.h>
#include <stdio.h>

int main() {
  int i = 0;

  printf("(++i) + (++i) = %d\n", (++i) + (++i));

  return 0;
}

コンパイルして実行すると、

$ gcc -std=c99 -pedantic -O0 -g tmp.c
$ ./a.out 
(++i) + (++i) = 4
$ gcc -std=c99 -pedantic -O6 -g tmp.c
$ ./a.out 
(++i) + (++i) = 4

が得られた*1javaだと3になりそうな式なのだが。

説明。

CFAQの3.2(http://www.kouno.jp/home/c_faq/c3.html)とよく似ている。向こうは後置++演算子同士の計算でこちらは前置++演算子同士の計算で、この式は副作用完了点を通過する前にiへの副作用が二回あるから未定義だ、という結論まで同じだ。
なので言語マニア的に設計面から理由付けをする。式の最中に複数の副作用が存在する場合の振る舞いとして選択肢をあげてみよう。

式の途中での副作用自体を禁止する
例えば、pythonでは代入が式ではなく文だ。代入文の中に代入文を入れ子にすることはできないので、式の途中で副作用が起こることはない。式の途中で呼び出した関数の中で副作用を起こすことはできるが、それは別の問題なので次のネタにでもしよう。
式の途中でも副作用を有効にする
例えば、Javaでは副作用のタイミングも引数の評価順序も厳密に定義されており、副作用が幾つあったとしても一切競合しない。JVMなどのスタックマシンを前提にする場合、副作用も逐次的に処理するように定義するのが自然だろう。
式の途中の副作用の競合を未定義とする
上の式のようにC言語が良い例だろう。パイプラインが深いCPUならば左右の式を並行して計算したり、左右の式があまりに複雑なものならば別々のコアが並行して計算したり、といった方式で速度効率を稼ぐ余地を残すことができる。C言語の場合は策定前に公開されていた実装の方式が統一されていなかったというのも理由かもしれない。
式の途中の副作用の競合をエラーとする
C言語仕様での「未定義」はどのような結果になるか判らないという意味、つまり、処理の結果が「コンパイルエラー」であっても良いのではないだろうか。ここまで厳しいコンパイラは聞いたことがないのは、副作用の競合を完全に検出するのは難しい、少なくとも時間がかかる、というのが主な理由だろう。

*1:gccの警告オプション-Wall -Wextraを付けていないのは答えが表示されてしまうのを防ぐため。