名前がおかしい。
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稼げて……という計算が必要だが、あくまでも複雑なスキル制という範疇で考えられるものになると思う。
〆。
試行回数を多めにした方が面白そうだと思うが、最適な試行回数が幾つなのかは分からない。とりあえず、試行回数が少ない方はずるずる続ける人を狙った月額払いっぽい。
それから、大技は永続コストを払わないとならない、というルールもありだろう。
挿入ソートの話のまとめ
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 $@
引数の評価順序。
ありがちダメなコードのダメな理由を書くコーナー。
現象。
#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の順に評価されがち、と思っていたのだけど違ったらしい。
副作用のタイミング
ありがちダメなコードのダメな理由を書くコーナー。
現象。
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
説明。
CFAQの3.2(http://www.kouno.jp/home/c_faq/c3.html)とよく似ている。向こうは後置++演算子同士の計算でこちらは前置++演算子同士の計算で、この式は副作用完了点を通過する前にiへの副作用が二回あるから未定義だ、という結論まで同じだ。
なので言語マニア的に設計面から理由付けをする。式の最中に複数の副作用が存在する場合の振る舞いとして選択肢をあげてみよう。
- 式の途中での副作用自体を禁止する
- 例えば、pythonでは代入が式ではなく文だ。代入文の中に代入文を入れ子にすることはできないので、式の途中で副作用が起こることはない。式の途中で呼び出した関数の中で副作用を起こすことはできるが、それは別の問題なので次のネタにでもしよう。
- 式の途中でも副作用を有効にする
- 例えば、Javaでは副作用のタイミングも引数の評価順序も厳密に定義されており、副作用が幾つあったとしても一切競合しない。JVMなどのスタックマシンを前提にする場合、副作用も逐次的に処理するように定義するのが自然だろう。
- 式の途中の副作用の競合を未定義とする
- 上の式のようにC言語が良い例だろう。パイプラインが深いCPUならば左右の式を並行して計算したり、左右の式があまりに複雑なものならば別々のコアが並行して計算したり、といった方式で速度効率を稼ぐ余地を残すことができる。C言語の場合は策定前に公開されていた実装の方式が統一されていなかったというのも理由かもしれない。
- 式の途中の副作用の競合をエラーとする
- C言語仕様での「未定義」はどのような結果になるか判らないという意味、つまり、処理の結果が「コンパイルエラー」であっても良いのではないだろうか。ここまで厳しいコンパイラは聞いたことがないのは、副作用の競合を完全に検出するのは難しい、少なくとも時間がかかる、というのが主な理由だろう。