[読書]どの口が東証システム(arrowhead)開発を指して正攻法だとのたまうのか『システム改革の正攻法』
「システム改革の正攻法」という東証の次世代株式売買システム「arrowhead」を立ち読みして、少し違和感を感じたので購入してみました。
パラパラとめくってみて、うーん、他の人はどんな感想なんだろう、と検索してみたところ、小飼弾さんの書評で
なにしろこちらにはソースコードの一部まで乗っているのだ。
という記述を見つけました。おおそんなものが書いてあったのか、と該当箇所を読んでみたところ、全く腑に落ちなかったのでまとめてみました。「第12章 スピード1000倍増の秘密 - 世界最速システムの全貌」 の 「マイクロ秒を惜しみ処理時間を削る」という節です。
p.132 図12-3に対応がまとめてありましたのでここから引用します。
- 設計段階で、業務ロジックを基に実行ステップ数を算出し、そこから処理時間を試算。目標時間を超えるものはステップ数削減のためにロジックや記述を見直す
○。ありだとは思いますが、この節のタイトル「マイクロ秒を惜しみ処理時間を削る」とはちょっとレベル感が違うような気はします。まあ正攻法ということで。
- アプリケーション内で複数のエラー検定(チェック)処理を行う場合、当てはまる確率の高い順に記述
- 業務処理やデータの特性に合わせた検索方法を採用
◎。これは、今回の発注者(東証)側の体制への取り組みを考えると、非常に効果的に機能したと想像します。
- 呼び出し順序が決まっているいくつかのソフト部品を一つに集約、関数の呼び出し回数を削減
×。関数のインライン化、ということでしょうが、一般的には正攻法ではなくバッドノウハウです。
アンチパターンでは肥満児(The Blob)という名前が付けられています(ただしこのパターンはOOD/Pに対して適用されるもので、本システムに対しては正確な名付けではありません)。また、リファクタリングのモチベーションとなる不吉な匂いの「長すぎるメソッド」にも分類されます。
正攻法としては、関数インライン化はコンパイラに任せることです(C99ではinlineキーワードが標準化されました)。
また、強引にインライン化した際のデメリットとしては、キャッシュメモリが有効に機能しなくなる、スラッシングが発生する、といったことが挙げられます。
「ソフト部品を一つに集約」という表現が曖昧で、もしかするとダイナミックリンクをスタティックリンクに変更する、LTO(Link-Time Optimization)が効かないコンパイラへの対応(コンパイラ事情には疎いのですが、gccでは比較的最近取り入れられたそうです)、といった内容にもとれますが、いずれにせよ保守性とのトレードオフになりますので諸手を挙げて正攻法とは呼べません(どちらかというとバッドノウハウに分類されます)。
- 複数のスレッドによるデータの共有を禁止(データを持ちまわりたい場合は、引数による受け渡しを利用)
×。速度向上につながる理由も分かりませんし、カッコ前後の文章のつながりも分かりません。一般的にはデータは共有した方がパフォーマンスは向上します。引数による受け渡しを行ったからといって共有が禁止できるわけではありません。スレッド間でデータを共有しない一般的な理由は、パフォーマンスに拠るものではなく並行処理におけるアクセス競合を防ぐためです。
- データ操作時の関数は「memxxx」を利用(「strxxx」関数は使わない)
○。strcpy_sを使わずにmemcpyを使用する、memmoveを使わずにmemcpyを使う、といった方針であれば、パフォーマンスとセキュリティリスクのトレードオフで前者を選択したのだな、ということが明確なのですが(ただしそれが正攻法であるかどうかは別問題です)。
要するに文字列を文字列として扱わず固定長バイナリとして扱う、ということなのですが、この対応を行うことの問題点としては
- 有効バイト以降(‘\0’以降)も意識する必要がある。本システム方針に沿って言えば、本来不要な部分の初期化、代入コストが発生する。
- 文字列を文字列として扱っている部分との境界(例えば周辺システムとのインタフェース)でも上記を意識する必要がある。
- 本来未初期化部分であるべきところを正しそうな値に置き換えてしまうことで、動的検査(例えばPurifyPlus)の能力を弱めてしまう。
といったことが挙げられ、個人的には△としたいです。しかし、現実のパフォーマンス差を見ると…という点で○です(私もこういう対応を行うことはありますし…)。
- フラグ(1バイトデータ)の比較には、「memcmp」関数を使わずに直接データを比較
- 1バイト以外のデータの比較には「memcmp」関数を使う
×。意味不明ですが、おそらくchar flg;
やchar str[10];
の場合の差異を言いたいのでしょう。しかしmemcmp
がとる引数はvoid*
型なのでそもそもchar
型変数を引数にとることはできません。char*
型変数が指す1バイトのデータ比較であれば、==を使おうがmemcmpを使おうがパフォーマンスは変わりません。
ところで、この記述を正確に読み取ると、int
みたいな組み込み型変数もmemcmp
を使うようになりますが(通常intは4バイト)、さすがにそんなことはやっていないでしょうね。
- データ項目一つひとつを初期化するのでなく、初期値をセットした変数を一度にコピー(図12-5参照)
後述します。
- 関数の引数にはポインタを使う、繰り返し処理内で無駄な記述をしない、冗長なチェック処理をなくす、無駄なログ出力を止める
○。当たり前と言えば当たり前なのですが、「正攻法」という観点なので。ただし繰り返し処理の中で変数宣言を行うな、といった最適化でどうとでもなるようなものだったら笑います。
ちなみに、私が担当したシステムでは、ログの重要度に応じて非同期で出力するか同期で出力するか切り替える、といったようなことも実装しました。JavaだとLog4jのAppenderで簡単に実現できみたいなんですけどね。
後回しにした構造体について。
この図からわかることを列挙します。
- 初期値の代入にはBGAA_Memcpyという関数が使われていますが、以下では文中の説明よりmemcpy相当として考えています。引数からみても、memcpyより高速なことを行っているようには見えないです。
- コメントのついていない Filter_m[n] はアラインメント調整用のパディングメンバでしょう。OSがLinuxでn=5のものがあるということは、64bitなんでしょうかね。
- アラインメントが決め打ちです。
- sizeof(int)などが決め打ちです。
- 初期値をchar配列で定義しており、サイズは構造体に合わせています。
これだけでも個人的には×をつけたいところですが、移植性に関しては考慮する必要が無いという判断なのでしょうし(longを使わずにlong longを使っているところを見るとあまり吹っ切れていないようにも見えますが)、保守性について重視していないこともこれまでの対応で明らかなので、ひとまず置いておきます。
構造体の初期化、というと通常は代入を使うわけですが、それよりもmemcpyの方が高速だ、という判断でmemcpyを使用しているのですから、双方比較してみれば妥当性は確認できます。
今回使用したコードは以下になります。構造体は図12-5の上半分を用いています。また、コンパイル環境はUbuntu10.04 32bit gcc-4.4.3になります。結果は-O3で最適化しています。
1: #include <stdio.h>
2: #include <stdlib.h>
3: #include <string.h>
4:
5: typedef struct {
6: char IssueKey[16];
7: char SecExKbn[1];
8: char MktKbn[2];
9: char IssueCd[12];
10: char IssueKbn[4];
11: char Filter_1[5];
12: long long Irate;
13: long long RepP;
14: char YldRstrn[1];
15: } LAA01_t;
16:
17: const static LAA01_t init_value = {
18: {0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20},
19: {0x20},
20: {0x20,0x20},
21: {0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20},
22: {0x20,0x20,0x20,0x20},
23: {0x20,0x20,0x20,0x20,0x20},
24: 0,
25: 0,
26: {0x20}
27: };
28:
29: const static char init_value_char[sizeof(LAA01_t)] ={
30: 0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
31: 0x20,
32: 0x20,0x20,
33: 0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
34: 0x20,0x20,0x20,0x20,
35: 0x20,0x20,0x20,0x20,0x20,
36: 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
37: 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
38: 0x20
39: };
40:
41:
42:
43: void assign_operator(LAA01_t *t1, const LAA01_t *t2){
44: *t1=*t2;
45: }
46:
47: void assign_memcpy(LAA01_t *t1, const LAA01_t *t2){
48: memcpy(t1,t2,sizeof(LAA01_t));
49: }
50:
51: void assign_memcpy_char(LAA01_t *t1, const char *c){
52: memcpy(t1,c,sizeof(LAA01_t));
53: }
54:
55:
56: int main(int argc, char** argv) {
57: LAA01_t t1, t2, t3;
58:
59: assign_operator(&t1,&init_value);
60: assign_memcpy_char(&t2,init_value_char);
61: assign_memcpy(&t3,&init_value);
62:
63: printf("%s[end]\n",t1.IssueKey);
64: printf("%s[end]\n",t2.IssueKey);
65: printf("%s[end]\n",t3.IssueKey);
66: return (EXIT_SUCCESS);
67: }
68:
上記コードの説明を簡単に行います。
- 初期値としてinit_valueという構造体を定義しています。
- 東証のコード相当の初期値はchar配列のinit_value_charです。
- assign_memcpy_charはmemcpy関数でchar配列から値をコピーする関数です。東証のコード相当です。
- assign_operatorは代入演算子で代入する関数です。
- assign_memcpyはmemcpy関数で構造体から値をコピーする関数です。参考として作成してみました。
つまり、assign_memcpy_char関数とassign_operatorの差が、memcpyを利用して初期値を設定することのメリットになります。上記コードをコンパイル後、objdump -dコマンドでアセンブラコードを表示し、比較しました。
アセンブラについての知識は全く無いに等しいので検証できるか不安でしたが、杞憂に終わりました。以下がそれぞれの関数の部分についてのコードになります。
1: 080484e0 <;assign_memcpy_char>:
2: 80484e0: 55 push %ebp
3: 80484e1: 89 e5 mov %esp,%ebp
4: 80484e3: 8b 55 0c mov 0xc(%ebp),%edx
5: 80484e6: 8b 45 08 mov 0x8(%ebp),%eax
6: 80484e9: 8b 0a mov (%edx),%ecx
7: 80484eb: 89 08 mov %ecx,(%eax)
8: 80484ed: 8b 4a 04 mov 0x4(%edx),%ecx
9: 80484f0: 89 48 04 mov %ecx,0x4(%eax)
10: 80484f3: 8b 4a 08 mov 0x8(%edx),%ecx
11: 80484f6: 89 48 08 mov %ecx,0x8(%eax)
12: 80484f9: 8b 4a 0c mov 0xc(%edx),%ecx
13: 80484fc: 89 48 0c mov %ecx,0xc(%eax)
14: 80484ff: 8b 4a 10 mov 0x10(%edx),%ecx
15: 8048502: 89 48 10 mov %ecx,0x10(%eax)
16: 8048505: 8b 4a 14 mov 0x14(%edx),%ecx
17: 8048508: 89 48 14 mov %ecx,0x14(%eax)
18: 804850b: 8b 4a 18 mov 0x18(%edx),%ecx
19: 804850e: 89 48 18 mov %ecx,0x18(%eax)
20: 8048511: 8b 4a 1c mov 0x1c(%edx),%ecx
21: 8048514: 89 48 1c mov %ecx,0x1c(%eax)
22: 8048517: 8b 4a 20 mov 0x20(%edx),%ecx
23: 804851a: 89 48 20 mov %ecx,0x20(%eax)
24: 804851d: 8b 4a 24 mov 0x24(%edx),%ecx
25: 8048520: 89 48 24 mov %ecx,0x24(%eax)
26: 8048523: 8b 4a 28 mov 0x28(%edx),%ecx
27: 8048526: 89 48 28 mov %ecx,0x28(%eax)
28: 8048529: 8b 4a 2c mov 0x2c(%edx),%ecx
29: 804852c: 89 48 2c mov %ecx,0x2c(%eax)
30: 804852f: 8b 4a 30 mov 0x30(%edx),%ecx
31: 8048532: 89 48 30 mov %ecx,0x30(%eax)
32: 8048535: 8b 4a 34 mov 0x34(%edx),%ecx
33: 8048538: 89 48 34 mov %ecx,0x34(%eax)
34: 804853b: 8b 52 38 mov 0x38(%edx),%edx
35: 804853e: 89 50 38 mov %edx,0x38(%eax)
36: 8048541: 5d pop %ebp
37: 8048542: c3 ret
38: 8048543: 8d b6 00 00 00 00 lea 0x0(%esi),%esi
39: 8048549: 8d bc 27 00 00 00 00 lea 0x0(%edi,%eiz,1),%edi
1: 08048470 <;assign_operator>:
2: 8048470: 55 push %ebp
3: 8048471: 89 e5 mov %esp,%ebp
4: 8048473: 8b 55 0c mov 0xc(%ebp),%edx
5: 8048476: 8b 45 08 mov 0x8(%ebp),%eax
6: 8048479: 8b 0a mov (%edx),%ecx
7: 804847b: 89 08 mov %ecx,(%eax)
8: 804847d: 8b 4a 04 mov 0x4(%edx),%ecx
9: 8048480: 89 48 04 mov %ecx,0x4(%eax)
10: 8048483: 8b 4a 08 mov 0x8(%edx),%ecx
11: 8048486: 89 48 08 mov %ecx,0x8(%eax)
12: 8048489: 8b 4a 0c mov 0xc(%edx),%ecx
13: 804848c: 89 48 0c mov %ecx,0xc(%eax)
14: 804848f: 8b 4a 10 mov 0x10(%edx),%ecx
15: 8048492: 89 48 10 mov %ecx,0x10(%eax)
16: 8048495: 8b 4a 14 mov 0x14(%edx),%ecx
17: 8048498: 89 48 14 mov %ecx,0x14(%eax)
18: 804849b: 8b 4a 18 mov 0x18(%edx),%ecx
19: 804849e: 89 48 18 mov %ecx,0x18(%eax)
20: 80484a1: 8b 4a 1c mov 0x1c(%edx),%ecx
21: 80484a4: 89 48 1c mov %ecx,0x1c(%eax)
22: 80484a7: 8b 4a 20 mov 0x20(%edx),%ecx
23: 80484aa: 89 48 20 mov %ecx,0x20(%eax)
24: 80484ad: 8b 4a 24 mov 0x24(%edx),%ecx
25: 80484b0: 89 48 24 mov %ecx,0x24(%eax)
26: 80484b3: 8b 4a 28 mov 0x28(%edx),%ecx
27: 80484b6: 89 48 28 mov %ecx,0x28(%eax)
28: 80484b9: 8b 4a 2c mov 0x2c(%edx),%ecx
29: 80484bc: 89 48 2c mov %ecx,0x2c(%eax)
30: 80484bf: 8b 4a 30 mov 0x30(%edx),%ecx
31: 80484c2: 89 48 30 mov %ecx,0x30(%eax)
32: 80484c5: 8b 4a 34 mov 0x34(%edx),%ecx
33: 80484c8: 89 48 34 mov %ecx,0x34(%eax)
34: 80484cb: 8b 52 38 mov 0x38(%edx),%edx
35: 80484ce: 89 50 38 mov %edx,0x38(%eax)
36: 80484d1: 5d pop %ebp
37: 80484d2: c3 ret
38: 80484d3: 8d b6 00 00 00 00 lea 0x0(%esi),%esi
39: 80484d9: 8d bc 27 00 00 00 00 lea 0x0(%edi,%eiz,1),%edi
1: 08048550 <;assign_memcpy>:
2: 8048550: 55 push %ebp
3: 8048551: 89 e5 mov %esp,%ebp
4: 8048553: 8b 55 0c mov 0xc(%ebp),%edx
5: 8048556: 8b 45 08 mov 0x8(%ebp),%eax
6: 8048559: 8b 0a mov (%edx),%ecx
7: 804855b: 89 08 mov %ecx,(%eax)
8: 804855d: 8b 4a 04 mov 0x4(%edx),%ecx
9: 8048560: 89 48 04 mov %ecx,0x4(%eax)
10: 8048563: 8b 4a 08 mov 0x8(%edx),%ecx
11: 8048566: 89 48 08 mov %ecx,0x8(%eax)
12: 8048569: 8b 4a 0c mov 0xc(%edx),%ecx
13: 804856c: 89 48 0c mov %ecx,0xc(%eax)
14: 804856f: 8b 4a 10 mov 0x10(%edx),%ecx
15: 8048572: 89 48 10 mov %ecx,0x10(%eax)
16: 8048575: 8b 4a 14 mov 0x14(%edx),%ecx
17: 8048578: 89 48 14 mov %ecx,0x14(%eax)
18: 804857b: 8b 4a 18 mov 0x18(%edx),%ecx
19: 804857e: 89 48 18 mov %ecx,0x18(%eax)
20: 8048581: 8b 4a 1c mov 0x1c(%edx),%ecx
21: 8048584: 89 48 1c mov %ecx,0x1c(%eax)
22: 8048587: 8b 4a 20 mov 0x20(%edx),%ecx
23: 804858a: 89 48 20 mov %ecx,0x20(%eax)
24: 804858d: 8b 4a 24 mov 0x24(%edx),%ecx
25: 8048590: 89 48 24 mov %ecx,0x24(%eax)
26: 8048593: 8b 4a 28 mov 0x28(%edx),%ecx
27: 8048596: 89 48 28 mov %ecx,0x28(%eax)
28: 8048599: 8b 4a 2c mov 0x2c(%edx),%ecx
29: 804859c: 89 48 2c mov %ecx,0x2c(%eax)
30: 804859f: 8b 4a 30 mov 0x30(%edx),%ecx
31: 80485a2: 89 48 30 mov %ecx,0x30(%eax)
32: 80485a5: 8b 4a 34 mov 0x34(%edx),%ecx
33: 80485a8: 89 48 34 mov %ecx,0x34(%eax)
34: 80485ab: 8b 52 38 mov 0x38(%edx),%edx
35: 80485ae: 89 50 38 mov %edx,0x38(%eax)
36: 80485b1: 5d pop %ebp
37: 80485b2: c3 ret
38: 80485b3: 8d b6 00 00 00 00 lea 0x0(%esi),%esi
39: 80485b9: 8d bc 27 00 00 00 00 lea 0x0(%edi,%eiz,1),%edi
全て同一です。ちなみに最適化しない場合は、memcpyを利用するとインライン化されず関数コールが増える分、memcpyを利用する方がコスト増です。
今回はソースコードを書いて試してみましたが、そもそも、なぜコンパイラは最適な代入処理を行ってくれない、という考えに至るのでしょうか。
上記のような対応を行った結果、保守性、移植性を低下させたにも関わらず、パフォーマンスには全く寄与しない(どころか低下させる可能性がある)ものになっています。評価としては二重×どころでは済みません。
これまで示してきた内容より、この書籍が言うところの「正攻法」とは、バッドノウハウを駆使してでもarrowheadの第一優先目標である速度向上を叶える、というものです。しかし一般的にはそれを正攻法とは呼びません。
以上、小飼さんの書評がきっかけということもあり、久しぶりにこちらを意識して書いてみました。
最後に。本書の中で鈴木義伯東証CIOが「技術者冥利に尽きる」とおっしゃっていらっしゃることについて。
このような巨大システム構築プロジェクトを予定通り(で良いんでしたっけ?当初は2009年秋だったような気もしますがこれは忘れます)完遂できた、情報システムの最高責任者であるCIOのプロジェクトマネジメント能力は並大抵のものではないと思われます。正直、私は2010年1月4日にarrowheadが稼働できるとは思っていませんでした。
しかし、私が同じような立場に立ったとするならば、目標を達成するために上で述べてきたような(仮に全て速度向上に繋がるとしても)泥臭い対応を行わなければならなかったことや、21世紀にもなってこのような保守性の低いシステムを見続けなければならないだろう若いSEのことを考えると、「技術者冥利に尽きる」などとは決して言うことはできないでしょう。万が一そのような言葉を吐かなければならない状況が予想されるのであれば、ここで記載したような対応をオープンにはしないでしょう。
この「技術者」についての価値観の違いが、冒頭に記載した違和感の正体かな、と考えます。
というか冒頭で示した小飼さんの書評、
全く奇をてらったことがない。まさに正攻法。愚直といってもいい。
自身で示した箇所の内容もちゃんと読んでませんよね…?
プログラミング的な部分以外にも言いたいことは出てきそうな予感がしますが、それはまた気が向けば。
« さきゅばす/coroid用ffmpegビルドpatch更新 | トップページ | [読書]彼女と二人で「C」体験!にあえてツッコむ »
この記事へのコメントは終了しました。
トラックバック
この記事へのトラックバック一覧です: [読書]どの口が東証システム(arrowhead)開発を指して正攻法だとのたまうのか『システム改革の正攻法』:
» [読書]彼女と二人で「C」体験にあえてツッコむ [雪羽の発火後忘失]
彼女と二人で「C」体験! (MF文庫 J い 2-7) 石川ユウヤ ストーリー上の小道具なわけで、無粋かとも思いましたので淡々と。前エントリからの流れということで… 出てくるソースはこれだけです。 1: #includeempirex.h 2: class CHeavyInfantry : public CHoplite 3: { 4: public: 5: int ReflectAnyStrik... [続きを読む]
» [読書]約定率が低いことは良いことか『システム改革の正攻法』 [雪羽の発火後忘失]
システム改革の「正攻法」 大和田 尚孝 正確には、システムの性能が上がったことを外部の人に示す指標として約定率の低下を用いるのは適切なんだろうか、という疑問です。 本書『システム改革の正攻法』の冒頭で、東証新システムarrowhead導入の効果について、以下のように記述されています。 arrowheadの導入効果は、早くもデータに表れている。代表例が東証の株式市場における「約定率」の変化だ(図1-... [続きを読む]
« さきゅばす/coroid用ffmpegビルドpatch更新 | トップページ | [読書]彼女と二人で「C」体験!にあえてツッコむ »
コメント