2009年11月28日

新人じゃなくなった人は何を気にするべきか -3- Scalable Organization

あなたの会社は、今、年間の粗利が n 円だとしよう。粗利だから売値から原材料費を引いた残りだね。それが n 円。

この n 円を稼ぐために、m人の「下っ端従業員」が必要だとする。平社員だけじゃなくアルバイトとかも含めて m人だ。従業員とかを管理する、管理職は m人 の中には含めない。ただし、部長なんだけれど仕事の1割はお客様の所に行くことです、なんて場合もあるだろう。その場合 m人の一部として 0.1 人と考える。ようするに n 円を稼ぐ原動力になっている人たちの頭数だけを数えて m人を算定する。mが大きくなってくると人事とか、総務とかそういう仕事も多くなってくるだろうが、それらの人たち無しでは n 円は稼げなかったのだから、それも mに含める。だけど、「営業マンを束ねる営業部長」とか、そういう人たちの「管理職務分」は含めない。

さて。ここで、n の値が 10倍、100倍、1000倍になったとしよう。mの値はそれぞれどれだけの大きさになるだろうか? ここで市場がそんなにでかくならないとか、そういう話にはならないことにしよう。10000倍にだって簡単になる市場において、粗利を増やすと直接必要な頭数はどう増えるか、という問題だ。

単純に mも 10倍、 100倍、 1000倍になります という組織構造だったら、早晩その会社はつぶれる。
判りやすくするために、今 m = 1 …つまり一人で1億の粗利を稼いでいるとしよう。粗利を合計で10億にしたい。そこでお客様の所に行く営業マンを10倍の10人にしたとする。m=10だ。
一人であれば、営業以外も全て自分ひとりでこなしていただろうが、10人の営業マンがいたら指導や管理をしなくちゃいけなくなる。そのために管理職という仕事が出てくる。ここで仮に10人の営業マンを管理するには一人の管理職…部長としようか…が必要だとしよう。すると、会社にいる頭数の合計は11人になる。
100倍にするとなると100人の営業マンが必要だ。10人の営業マンごとに一人の部長が必要なので、部長が10人必要になる。すると、それらの部長の意思統一を図り管理監督するために部長を管理する管理職…専務としようか…が必要になる。すると、会社にいる頭数の合計は 100+10+1 = 111人になる。
1000倍にするとなると、1000人の営業マンと、100人の部長と、10人の専務が必要になる。10人の専務の意思統一を…もういいだろう?…ようするに一人の社長が必要になる。会社にいる頭数は 1111人だ。

粗利が1000倍の1000億にしかなっていないのに、人手は 1111人必要だ。この粗利の中から1111人分の給料を捻出する必要がある。最初は一人1億のなかから給料を捻出すればよかったのに、今は9千万円から給料を捻出しなくちゃいけない。仮に給料を変えないとすると…売上をどんどこ増やしていくとどこかで赤字になる。赤字にしないためには給料を減らさなくちゃいけない…

あれ~??
売り上げを伸ばして粗利を増やしたのに
給料減っちゃったよ???

会社としての収入の伸びを下っ端の頭数に依存した価格設定にしている会社は、必ずこうなる。
多くの場合、グループ企業の子会社とかは、親会社に対して「何人がかりで何時間働いたのでいくら」というお値段のつけ方しかできなかったりするが、実はこれは子会社が大きくなれないようにするための秘策だったりするのだ。もしあなたが勤めている会社がお客様にそういうチャージの仕方しかしていないなら…転職を考えたほうが良い。規模が大きくなるほど忙しくなるが、儲けはむしろ減っていくのだから。

この状態を回避する方法は全部で4つある。
  1. 規模を大きくしない
  2. 定期的に組織構造を根こそぎ書き換える
  3. mがnに比例している部分をアウトソースしてしまう
  4. nを増やしても比例してmが増えたりしないようにする
1の「規模を大きくしない」と言うのは「京都で600年間続く老舗」とかではよくある。手広く商売するのではなく、限定された規模のお客様だけを相手にし、その代わりそのお客様を手放さないようにする。これを可能にするためには、収入を他人と分け合わない 必要がある。株式公開なんかもってのほか。借金をしたら利子を取られるのでそれもなし。何らかの希少性を前提にできるならば、この戦略はありだ。
ただ、一般的な企業はそうは行かない。株式は公開されているし、拡大再生産しないと経営陣が突き上げを食らう。

2はようするに「リストラクチャリングを定期的に行う」と言うものだ。ようするにこれは富の再分配の問題なのだから、会社の構造を書き換えて、再分配ルールを変更してしまえばよい。
ただ、これをやると必ず「今までよりも手取りが減る」人が出てくる。そりゃそうだ。そのために構造を変更しているんだもの。大抵この手のしわ寄せは「下っ端」に押し付けられる。問題はほぼ全ての職種で「下っ端」はいくらでも交換可能な存在 ではない という点だ。旧来の高給取りを首にして、安い給料で働く人間を入れると、必ず一人当たりの粗利自体が悪化する。市場で急激に売り上げを伸ばしたので会社の構造を変えたら、品質が悪化、売り上げが縮退し、そのまま倒産…なんて会社があるが、それは リストラに失敗した という事だ。この手段は意外とリスクが大きい。

3はよくある「グループ企業」って奴だ。自分では構造改革できない部分を切り離して外に押しやり、
「自分たちでどうにかしろ」
というかなり無責任な行為。もしあなたが、「人月」でしか値段をつけられないのにこのようにアウトソースされたのなら、すでに万策は尽きている。親会社の方を切り捨てるしかない。他の企業は人月によるチャージにならないよう、値段交渉できるが、親会社は政治的圧力をかけてくるからだ。

で。結局3の場合も独立したあとは 4 の問題を解かなくちゃいけない。10倍の粗利を得るのに10倍未満の人口増でまかなわなくてはいけないのだ。最低限度でも 管理職をも含めた全人数 が粗利の倍率に比例するように、人数が増えると作業効率がよくなるようにしなくてはいけない。

Automation の本領発揮だ

Scalability を確保する方法は、今知られている範囲では、3種類ある。
  1. 分割
  2. 機械化
  3. off demand
…あう。ずんどこ書いていったら終わらないよ。というわけで上記3種類、それぞれの詳細は次回。
ただ、上記3種類、いずれも「無限の scalability」は持っていない。どうしても 一定の n の範囲内で という条件を外すことができない。単に発見されていないだけなのか、存在しえないのかは、不明。

2009年11月25日

新人じゃなくなった人は何を気にするべきか -2- Market fundamentalism

まずは Automation の1つ目。市場原理主義。Market Fundamentalism。

うん。普通は経済の用語だよね。だから Customer Satisfaction 側の説明じゃないのか? と疑問に思ったあなたは、おそらく市場原理主義をある程度理解している。が、理解が足りない。

Automation を理解するうえで最も重要なのは何か? Automation の究極奥義とは何か。それは

放っておいたらどうなるか知ること

だったりする。手に持っているボールを放す。すると重力に引かれて落ちる。まずこれを理解するからこそ、「ボールが落ちないようにするにはどうすればいいか」という発想が生まれる。物事を制御する、と言う考えが生まれる。で、常にそれを意識してやってなんぞいられないから、自動化する。

市場原理主義というのは、人間を放っておいたらどうなるか、そしてそれは何故か、を理解する上でとても重要な知恵だ。何故これが大事か? だってあなたは今、後輩を持ってるんですぜ? 部下を持ってるんですぜ? 組織の親玉として、組織全体がどう動くのかに責任を持ってるんですぜ?

こいつら、放っておいたらどこへ行く?

って知るのは大事じゃないかい? さらに、それが自分の目的と合致していたら? 難しく考えなくても、放っておくだけで目的が達成されるって事だよね?! いちいち、あーしろ、こーしろ、あーするな、こうするな、と指示しなくても自動的に目的が達成されるなら、これ以上楽なことはないじゃないか。

もちろん、世の中そんなに甘くなくて、一事が万事放任主義でどうにかなったりはしない。しかし、だからといってルールをガチガチに作って、社則で縛り上げたら目的が達成されるかと言うと、これまた間違い。



これを理解するには…そうだな。セキュリティが一番判りやすいだろう。USBメモリーについて考えてみようじゃないか。

昨今では、多くの人が理解していると思うが、USBメモリーと言うのは非常に危険なデバイスだ。基本的に不揮発性の記憶媒体だが、ものすごく小さくて持ち運びしやすい。故に、輸送したいデータなんかを入れて、思わず持ち運んでしまう。

しかし、そのようなデータの中にはお客様情報などの、プライバシーやら守秘義務やらに関連したデータがありえる。つーか、そういうものを運ぶ上でUSBメモリーというのは非常に便利な輸送デバイスだったりする。なにしろ、インターネットのような「どこ通ってるんだか判らなねー」ようなものを経由しない。容量も結構でかい。思わず使って、USBメモリーごと失くして、さぁ大変、ってな状態に陥る。

だから、大抵の大手会社は USBメモリーの利用を制限したり、持ち込み自体を制限したり、と言ったルールを使っているはずだ。

にも拘らず。USBメモリーを失くした、という失敗談が減りもしなければ無くなりもしない、と言うのも事実。USBメモリーを作っていた会社が、売れ行き不振で倒産したという話も聞かない。という事は、それだけ多くの人が、USBメモリーを 禁止された後も使っている って事だ。

ここでは、事の是非を議論したいわけではない。USBメモリーを禁止するべきじゃない、と言う話をしたいのでもない。議論したいのは、 何故、USBメモリーは使われ続けているのか? という事だ。禁止されているんだから、おそらく破ったら罰則があるはずだ。にも拘らず、使われ続けるのは何故?

別の事例を考えよう。仮に、USBメモリーを使っても良いとする。今あなたの手元に X というファイルがあるとしよう。
「Xのコピーを頂戴よ」
と私が言ったとしたら? 多分あなたは USBメモリーを取り出してそいつにXをコピーし、
「はい」
と言って渡してくれるだろう。私は USBメモリーを自分のPCに挿し、Xを取り出して、USBメモリーは返す。

じゃぁ、
「Xのコピーを、a, b, c, d, e, ... さん達に渡してあげてくれない?」
と頼んだら? あなたは USBメモリーを使うだろうか?? 多少は環境にも寄るだろうが(メールに添付できるファイルのサイズとかに上限があるかもしれない)、それら、コピーを渡さなくちゃいけない人たち全員を To か Cc に入れたメールに、添付ファイルとして X をつけて、一気に送るんじゃないだろうか?
間違っても USBメモリーを回覧したりはしないと思う。

はい、ここで質問。 最初のケースではUSBメモリーを使ったのに、2つ目のケースではUSBメモリーを使わなかったのは、何故?

最初のケースでは USBメモリーを使うのが最もコスト対効果がある、と判断されたから。2つ目のケースでは USBメモリーを使うよりもメールを使ったほうがコスト対効果が高い、と判断されたから。だから、1つ目のケースでは USB メモリーを使ったし、2つ目のケースでは USBメモリーを使わなかった。
同様に、セキュリティルールで禁止され、罰則まで規定されているのに USBメモリーを使う人が後を絶たないのは、罰則までをも含めてもなお、USBメモリーを使うほうがコスト対効果が高い、と判断されたからだ。

実は、ここには市場原理が働いている。USBメモリーはその市場原理に基づいて、ある条件下では採用され、別の条件下では採用されなかったのだ。禁則・罰則の導入はUSBメモリーを完全に排除できるほど、市場に対する影響力は無かったのだ。


後輩や部下の面倒を見る上で重要なのは、あなたの後輩や部下は、市場原理に基づいて動いている事を理解することだ。ルールや罰則は、ある程度は市場に影響を与えるが、完璧からは程遠い。そのことを理解しないで、ルール策定にまい進する偉い人が非常に多いが、そんなのは何の役にも立ちはしない。

市場原理主義 が Automation を考慮するうえで、第一番目に来る理由はここにある。

法律と罰則を持って人の行動を拘束すれば、良い世の中ができる、と信じた人たちがいる。法家といわれる人たちで、秦の始皇帝の時代の少し前、春秋戦国時代の百家のひとつだ。彼らは、悪しき行いを法で禁じ、法を破るものには厳罰をもって処す事にすれば、必ずや人々は法に従い、この世はすばらしいものになると信じた。今から2200-2300年ぐらい前の話だ。もちろん結果は大失敗

法を破る行為をするかしないか、逡巡している人がいるとしよう。法を破ると罰則以外のメリット、デメリット全部勘案して +10 の効果が得られるとする。法を破らないと 0 だ。もし、ここで罰則が -10 よりも大きくなかったら、どんな人でも毎回法を破ろうとするだろう。だから罰則は -10 かさらに厳しいのががふさわしい。じゃぁ、どれぐらい大きければいいのか?
まず、最初に考えなくてはいけないのは、罰則の適用は法を破ったときに必ず行われるわけではない、と言うこと。法を破ったことが発見されなくちゃいけない。たとえば10回に1回ばれるとするなら、「罰則を食らう期待値」が -10 を超えなくちゃいけないから、-100 かさらに厳しい必要がある。逆に、この値が厳しくても、法に反しない人たちにとっては何の影響もないはずだ。だから厳罰で構わない。法家の人たちはそう考えたわけだ。

しかし、実際にはこのようなルールを適用するとなると、ルールを理解しそれに基づいて人々を監視する者が必要になる。監視役が甘ければ発見確率は著しく小さくなり、どんな罰則を以ってしても法を守ることは割に合わなくなる。さらに、罰則を強化すると、
監視役が違法を見逃す代わりに賄賂を受け取る
という状態が始まった。ようするに役人の腐敗がスタートしたわけ。監視役を複雑に強化して相互監視させ、さらに秘密警察まで用意した国では、密告が横行し、さらに嘘の密告が横行しだして、さらに法の運営が崩壊してしまった。何のことは無い。罰則を強化したために Premium Cost と呼ばれるものが発生し、それが新たなる闇市場を作り出してしまったわけだ。

これは USBメモリの場合についてだっていえる。原則がUSBメモリーの利用禁止だったとしても、それを禁止しすぎたら仕事が動かなくなる。だから皆互いに見てみぬ振りをする。このような状態が横行すると、社則などによる禁止項目全体が運営の危機にさらされる。

さらに監視するためのコスト、と言う問題もあるのだけれど、それは、この次の Scalable Organization の所で言うことにしよう。

とにかく。厄介なのは、法で禁止して、厳罰を科せば、何かが実行されなくなると思ったら大間違いだ、と言うこと。罰則で何かがなくなることは無いし、単なる禁則は何であれ地下に潜らせる、と言うのが 自然な流れだ という事を理解しなくちゃいけない。

じゃぁ、打つ手無しなのか? そうじゃない。

USBメモリを使っているシーンと、使わなかったシーンをもう一度思い起こして欲しい。

一人に対してファイルを渡す場合は、USBメモリを使った。
多人数に対してファイルを渡す場合は、USBメモリを使わなかった。

何故?

多人数の場合は、USBメモリはオーバーヘッドがでかかったからだ。逆に言うと、
一人にファイルを渡す際にネットワークを使うのは、
オーバーヘッドが大きすぎる
という事だ。多分、100Mbpsぐらいしか出ない上に、共有ドライブのIO速度も遅いのだろう。

じゃあ。ネットワークが 10Gbpsぐらい出て、共有ドライブのIO速度が USBメモリの 10倍ぐらい早く、なにより容量が一人当たり Tbyte クラスだったら? USBメモリを取り出してPCに挿すほうが余程オーバーヘッドだわな。

ファイルのコピーを渡すのではなく、そのファイルを作成・管理している SubVersion Repository を教えるのならどうだろう?
SubVersion はファイルを保存するたびに、差分だけをサーバーに送りつける。このため、10Gbps などという、まだちょっと無理目な性能じゃなくても十分早く機能する。過去のバージョンを差分管理しているので、昔のバージョンまでも全部保持しているのに、全ファイルの合計サイズよりもはるかに小さい共有ディスク容量しか必要としない。10Gbps+Tbyteディスクよりは現実的な解だ。

そのような環境では、USBメモリが使われる可能性は大幅に減るはずだ。大幅に減る、という事はUSBメモリを持っている人自体少ない、という事を意味する。

このように環境を整備してから、
USBメモリを禁止したら
どうなるだろう?!

罰則をはるかに軽くしても、違反者はほとんど出ないはずだ。だって、使う価値の無いものを禁止されてるんだもの。「禁止されなくたって、使わないよ」と言う人がほとんどな状態で、禁則を犯す人はほとんどいない。だって、メリット無いもの。市場原理に従って、USBメモリ利用者はほとんど出ない

市場原理に従って、USBメモリを利用するものがいないのだから、USBメモリを利用していないかどうか、監視する必要もない。監視者がいないし、違反者も見つからないので、監視者の腐敗も起こらない。監視するためのコストも要らない。となると、環境を整備するためのコストは、実は十分おつりが来るぐらい安いのだ、と言うことも判る。



市場原理主義に立つと、「行政と司法の等価交換」と言われる行為が取れるようになります。

法というのは大抵目的を持って設置されるわけですが、その目的を満たすのには、「法律を作り罰則を作り司法を強化する」方法と、「環境を整備する」方法がある。どんなときでも司法と等価な行政が存在するわけではありませんが、存在する場合は、司法の代わりに行政をもって目的を達成する事ができるようになる。しかも、環境を整えさえすれば、自然と目的が達せられる

実は、市場原理主義を十分利用しないと、Scalable Organization という2つ目の Automation ルールに抵触する事になります。そして、これに抵触すると、あなたの会社は大赤字で倒産する危険性が出てくるのです。

…というわけで、次は Scalable Organization の話です。

2009年11月24日

新人じゃなくなった人は何を気にするべきか -1- やっぱり CS と AT

昔書いた日記、「新社会人にあなたならまず何を教えるか? -1- CS と AT」から始まる10回 が思ったより好評だった。ので、久しぶりに続きを書こうと思う。今回は「新人じゃない人」は何を気にするべきかという話。

そもそもは、ある会社の社長さんが社内メールで悲鳴を上げたことに始まる。いや、その内容は内緒だが、ようするにそれを見ていて思ったのは、
「そういえばこの会社、新社会人以外も問題のある人が多いよな」
というもの。なに? お前もだ? うむ。その通り。だから、今回の話を書くに当たっては、まず、心の中に 大きな、大きな大きな 棚を作った。そりゃもう ギアナ高地 のような、大きな奴だ。どうにか自分をそこに置くことに成功したので、棚が壊れる前に先に進む。

で、いろいろ考えたわけだ。どういう事を気にするべきかとか、どういう問題があるかとか。で、いろいろまとめていくと…結局新社会人に教えたことと同じ枠組みから一歩も外に出る必要がない、という事に気が付いた。やはりお金を稼ぐには2つの項目:
  1. Customer Satisfaction
  2. Automation
の2つに神経を集中する必要がある。逆に、この2つのためにその他の注意すべき項目は存在するし、それらに関してどうやって解くべきか、を考えるとどうやら答は一通りではないようだ。各々、自分が好きな、得意な解き方というのがあってよいように思う。


で。今回の議論。新社会人が相手じゃない。社会人になって、後輩とか部下とかができる辺り…って2年目からジャン…以降の人、社長さん(つーか CEOな)の辺りまでが考慮対象だ。活躍する経済圏としてはまだ開放系を前提とする。世界中を全部1つの経済圏として見なくてはいけない人(銀河皇帝になる私とか)は、経済圏を閉鎖系として見なくちゃいけないのだが、そこまで大きなものを見据えなくちゃいけない人たちは考慮対象としない。つーか下手にその辺を教えると、支配者のライバルが続出するしな :p

経済圏が開放系だ、という事は、あなたの活動によって世の中の経済が大躍進を遂げたり、逆に大打撃を受けたりはしない、という事を意味する。あなたの会社の趨勢はあくまでも局地的な問題で、あなたの会社が上手くいかなかったら、他の会社がその位置を占めるだけのこと。そういう大前提を念頭に置いてほしい。

また、「新社会人にあなたならまず何を教えるか? -1- CS と AT」から始まる10回 で述べたことは繰り返さない。基本的に、あそこに書いたことはすでに知っている、という辺りからはじめる。でも、同じことは書くかもしれない(どっちやねん)。いや、新人の時とは違う意識を持って同じ事をね…

書く内容はこんな感じ:
  1. Automation
    1. Market fundamentalism (市場原理主義)
    2. Scalable Organization
  2. Customer Satisfaction
    1. Who's Customer(で、お客って誰さ)
    2. Communication(Transfer の大事さ)
    3. Annoucement
    4. I18N/L10N
さて。今回の議論だが、 Automation から始めよう。考慮しなくちゃいけない項目は Customer Satisfaction の方が多いのだが、そうなる理由は Automation 側のニーズが起因となっているものが多いのだ… と言うわけで次回に続く。

2009年9月27日

【DQ9】 くもの大王 ハンティング

今度は いかずちのたま が足りない。これをもっているのは くもの大王ヘルクラウダー 。が、ヘルクラウダーは強すぎる。いきおい、 くもの大王 を狙うことになる。

で、くもの大王、実は結構あちこちにいる。いるのだが、「シンボルとして出てくる」のではなく、別のモンスターにくっついて出てくる 事が多い。これだと偶然を待たなくてはいけないので困る。宝の地図 だと、もっと強いモンスターが同じフロアにいたりして、これまた困る。あくまでも地上マップで済ませられるならば、済ませたい。

で、あちこちうろついて探した結果、見つけたのがこちら:

丁度この丸でかこんである部分。竜のしっぽ地方の中でもこの部分だけ、くもの大王がシンボルで出てきます。

一緒にうろついているのは、主に サイクロプスメガザルロック です。 また戦闘シーンにだけ出てくるモンスターとして、 ベホイムスライムギガントヒルズヒートギズモ がいます。いずれも雑魚ではありますが、いつまでも生かしておくと戦闘ターンが長くなる元です。とっとと倒してしまいましょう。ただし、勢いに乗りすぎて くもの大王 まで殺してしまわないように。

なお、 くもの大王 はやたらと「いかずちを呼び寄せる」技が好きです。と言うか、もうほとんど毎回そればかりです。装備を 雷耐性 のある方向にし、いやしのうでわ が毎ターン補充できる程度の被害に収まるようにしておくとよいでしょう。

【DQ9】 青空スローライフ な おっきー さんからのお便り

なんか最近、いじめられているような気がするんです。

たとえば、今日も「怒れる花の地図 Lv.45 発見者: かりん」という地図に潜っていたのです。
きっとこんな端っこの方なら誰も来ないだろうなって思って。
本当に端っこにいたんですよ。
じっとしてました。
本当です。
なのに…
普段は、
人の気配を感じただけですぐ逃げ出すはずの、
はぐれメタルさんまでが
ガンをつけにくるんです…

…あぁ、また…

2009年9月25日

【DQ9】おどるほうせき ハンティング

DQ9をやっている。

ま、一応メインストーリーは終わったのですが、皆様ご存知の通り、このゲームは そこからが 本番。クエストとか、宝の地図とか、錬金コンプリートとか、いやそれどころか錬金しないと得られないアイテムだとか…もう盛りだくさん。

一説には「クリスマスにロックが解除される、きわめてシーズンを狙った追加クエスト」というものまであるらしく…半年以上引っ張るつもりか… SQUARE ENIX。FF12 とバッティングするぞ?!!

で、やってらっしゃるとわかると思いますが、問題の一つが錬金。ハイパワーな武器、鉄壁の防具の類を作ろうとすると、材料がすでに錬金した結果のもの。それらを作るためにさらに材料を集める…と、いくつもの材料が圧倒的に不足する。

「xxxx を作るには yyyy が 3つ必要なんだけど、yyyy を作るには zzzz が 3つ必要」

ってこの段階で、zzzz が9つ必要やんけっ!! などというのはざら。これをキャラクター人数分作る段階で4人分…つまり36個。で、実はこれが2段階程度じゃすまなかったりする。
売ってるものならまだいい。Goldを稼げば話は終わる。でも拾ってくるしかなかったりすると…。


というそのようなアイテムの一つが ひかりの石 。もちろん、フィールドにも落ちている場所はあるけれど、一度に拾えるのは6個とかその程度。その後30分ぐらいは復活しないのだ。それより おどるほうせき というモンスターに ぬすむ を仕掛けた方がよほど効率が良い。もし おどるほうせき がどこに出没するのかさえ判れば。

もちろん判っていて、一つ目が カルバド大草原 。ただし、ここはとても広くて「待ち伏せる」なんてできない。しかも出没頻度がものすごくレア。で、二つ目がこれから紹介する 魔獣のどうくつ 。ここでも出現確率はレアなのだが、良いポイントを見つけたのだ。これからそれを紹介しよう。

魔獣のどうくつ がどこなのか?世界地図を紹介しているページがあるので探してくれ。
一応、左はしに近い、上下ではまんなから辺だ。まさか、自宅のすぐそばにこんな大事な場所があったとは…。

で、魔獣のどうくつの B1F が目的地。待機ポイントはここ:
魔獣のどうくつ B1F の、地図ほぼ一番下。

で、これが待ちの体制。

この場所の近くで下向きキーを押すと自動的にここに ハマる 。で、ここに到達したらすぐ X ボタンをおして、図のように「どうぐ」とかを選択するモードに設定する。こうしていると、モンスターはあなたの事を無視してくれる。余計な奴が出てきても、素通りしてくれるのだ。


で、最も多い出現パターンはこう:

私の感覚だと、だいたい 3/4 ぐらいがここに現れる。出現したら B ボタンをおしてダンジョンワールドに復活、即効ダッシュで捕まえる。

即効ダッシュで捕まえるには、あいだに余計なモンスターがいては駄目だ。一分ぐらい待つと、2,3匹よけいな奴らがうろつき始めるので、 掃除の意味をこめて モンスターを討伐する。


たまに発生するのがこれ。

みずらい画像で申し訳ない。このひん曲がった大福餅のような奴も、おどるほうせき だ。実は画面左側にもう1箇所モンスターの出現場所があり、そこから出てくる場合がある。

どうも一度に2つ以上の おどるほうせき シンボルが発生する事はないらしい。
「あれ~?出てこないなぁ」
と思ったら大抵左から出てくる、と思っていい。大雑把に30秒待っても最初のポイントに出現しないなら、このパターンが多い。


この場所、右側が坂になっており、普通は見えないはずのモンスター出現場所が2箇所、画面に映っている。また、左側にも出現場所があるのだが、こちらはモンスターにとっては行き止まりらしい。必ず左から右へ現れる。結果、3箇所の出現場所どこで発生したモンスターも捉えることができる。

これだけ出現ポイントがあると、モンスターを一発倒して出現しているモンスターをリセットしてから大雑把に30秒から1分ぐらいで、どこかしらで おどるほうせき は現れるらしい。

2009年8月30日

Firefox3の sqlite ファイルを vacuum して reindex する

Firefox3 は sqlite3 形式のデータベースを内部で使っているらしい。たとえば私のマシンだと
bash-3.2$ cd 'c:/Documents and Settings/*/Application Data/Mozilla/Firefox/Profiles/'
bash-3.2$ ls -alF default.jc3/*.sqlite
----------+ 1 okuyama なし 7168 Aug 29 21:41 ./default.jc3/content-prefs.sqlite
----------+ 1 okuyama なし 104448 Aug 29 21:41 ./default.jc3/cookies.sqlite
----------+ 1 okuyama なし 2048 Aug 29 21:41 ./default.jc3/downloads.sqlite
----------+ 1 okuyama なし 940032 Aug 29 21:41 ./default.jc3/formhistory.sqlite
----------+ 1 okuyama なし 5120 Aug 29 21:41 ./default.jc3/goodictionary.sqlite
----------+ 1 okuyama なし 2048 Aug 29 21:41 ./default.jc3/permissions.sqlite
----------+ 1 okuyama なし 1241088 Aug 29 21:41 ./default.jc3/places.sqlite
----------+ 1 okuyama なし 2048 Aug 29 21:41 ./default.jc3/search.sqlite
----------+ 1 okuyama なし 50176 Aug 29 21:41 ./default.jc3/signons.sqlite
----------+ 1 okuyama なし 2710528 Aug 29 21:41 ./default.jc3/urlclassifier2.sqlite
----------+ 1 okuyama なし 2048 Aug 29 21:41 ./default.jc3/webappsstore.sqlite

のように sqlite ファイルが存在する(cd するパス名中一箇所 * になっているのは本当はユーザー名)。上記はもうすでに作業しちゃった後なのでこれでも結構小さくなっているが、Firefoxをインストールしてちょっといじった直後だと、結構でかい。

で、どうやらこれを小さくする方法があるらしい。本当は slashdot.jp で最初にリンクを見たのだが、その日記がどこに行ったのか判らなくなったので、代わりに先ほど見つけた所を参考資料として。

http://d.hatena.ne.jp/dolphinkick/20090309/Firefox_Batch_SQLite_reindex_vacuum
http://www.gettingclever.com/2008/06/vacuum-your-firefox-3.html

ようするに sqlite3 を取ってきて、vacuum と reindex をしろ、と言うことだ。

  1. まずはここから sqlite3.exe を手に入れる。
    http://www.sqlite.org/download.html
    "Precompiled Binaries for Windows" から zip ファイルを持って来る。で、適当な場所にインストール。つーても zip を解くと、sqlite3.exe というファイルが出てくるので、それをどこか Path が通っているか、自分が判っている所に置く、というだけですが。

  2. すでに出てきているように Application Data のどこかに Firefox3 の sqlite ファイルがある。拡張子は「.sqlite」なのでそれを見つける。私は Cygwin を入れているので、unix の find コマンドが使える。のでそれで
    bash-3.2$ find . -name '*.sqlite'
    で発見できた。

  3. sqlite3.exe がカレントディレクトリにあるとして。あとはこう:
    bash-3.2$ for i in $(find . -name '*.*sqlite'); do
    > ./sqlite3.exe $i vacuum
    > ./sqlite3.exe $i reindex
    > done
    意外と簡単にずどん、と終わる。

前後でファイルサイズを比較すると判るが、これが結構なサイズダウンになる。

データベースのパフォーマンスは、「一定量 DBMSファイルを読む中にどれだけ Tuple が詰まっているか」と「Indexが適切に張られていて、無駄な読み書きを必要としない状態になっているか」が決める。

vacuum 命令は DBMS ファイルから無駄な空間を削り、Tupleを詰めてくれる。
reindex 命令は DBMSのどこにどの Tuple が置いてあるのかを示す、インデックスを作り直してくれる。

vacuumを実行すると Tuple の位置が動くので、reindex は vacuum の後でなくてはいけないはずだ。その辺が、参考にしたページとはちょっと違うところ。


速度向上ですか? まぁ、なんとなく早くなった気はしますが…そもそもが起動するまでが時間がかかるソフトですから…15秒が12秒になっても…うーん、どうよ、と言う感じ。とはいえ、まぁ、参考にはなります。

というわけで、私もあくまでも参考として。

2009年8月16日

無駄な謙虚さ

まぁ、なにしろこのような日記を書くぐらいですので、ときどき人から言われます。

「もっと謙虚になりなさい」

言う人に限って、
お前が言うか

というぐらい謙虚じゃないだけでなく、無能で、無駄なところでだけ謙虚な人なので無視していますが。今回はその「謙虚さ」についてのお話。


まず、ほとんどの人が信じられないでしょうが、私は謙虚であるべきときには謙虚です。「信じられないだろうが」と「謙虚であるべきときは」という条件が付く、と言うことは、
  1. ほとんどの場合は謙虚じゃない
  2. 「どういうときに謙虚であり、どういうときには謙虚であってはいけないか」について、ルールがある
という事です。さらに言うと 1 のせいで

観察力のない人から見れば、
謙虚な所が無いように見える


とまぁ、ここまで大見得を切れば当然答えなくてはいけないのが、

どういうときに謙虚なのさ
どうしてそれ以外は謙虚じゃないのさ
どうして普段は謙虚に見えないのさ
どうしてそれがいいのさ


というわけで、今回はそれに答える、というテーマ。

意識するかどうかはともかくとして。大抵の人は PDCAサイクル というものにそって行動している、と言われています。

Plan: 計画を立てる(報)
Do: 実行してみる (因)
Check: どうなったのか観察する(果)
Act: 計画との齟齬をチェックする、分析する(応)

ようするに、何かをしようとする前に『何をするかを決める』。何かをしたら『どうなったかを見る』。そして『分析する』。その結果を次の『何をするのか決める』段階に生かす。

実行してみる、の段階は、実際に実行しようとしてうまくいったり失敗したりします。ここには謙虚もへったくれもありません。もし、他者から見て、この段階で謙虚さが欲しいなら、それはその前の Plan が大胆すぎた、という事です。そして私に言わせれば、Plan の段階で謙虚さなんぞ発揮しても無駄です。Planの段階で謙虚だ、と言うことは 「やればできるかもしれないことに手を出さない」という事です。そんな所に謙虚さは不要です。

もちろん、失敗した場合のリカバープランは立てる必要があるでしょう。あるいはリスク分析をして、失うものについては覚悟が必要です。しかし得失を計算して、黒字になると考えたら、失敗覚悟でやってみる。この段階で萎縮する必要は何もありません。

問題は「正しく得失を予測できるか?」と言う点です
できないなら、何故?


実は、大抵の人間に欠落しているのは

観察(Check) の段階の謙虚さ
と、
分析(Act)の謙虚さ

都合の悪いデータを排除する。そのための言い訳を用意する。そのため、観察の段階では非常に都合の良い結果が出ます。しかし、自分自身は観察の段階に手心を加えたと知っていますので、その後の Plan において、分析結果をそのまま使うことができません。

どうしても安全サイドに倒した Plan になる
そのような Plan と Do を
謙虚と言いたくなる

しかし、これは謙虚さではありません。単なる誤魔化しと、他人も自分と同じような誤魔化しをやっているのではないか、という猜疑心です。

多くの人、特に私に謙虚さについて一言述べたくなる人は、大抵、このパターンのようです。



ちょっと前ですが、爆笑問題の二人が出ていた、転職サイトのCMにこんなのがありました。確か en-Japan だったと思うんですが…

結局、大胆だったのは、
いちばん慎重な人でした。

ここでいう「慎重な人」と言うのは、CheckとActの段階で十分な情報収集をする人、自分に都合の悪い情報もきちんと受け止める、入力に対して謙虚な人です。そういう人は、その次の Plan の段階で 大胆な手を打てる。自分が入力情報に対して謙虚だ、という事から来る自信こそが Plan と Do の大胆さにつながる。


というわけで。多分、これで答がわかったと思います。

私は Check と Act に対して謙虚です。
Plan と Do に関しては謙虚さなんぞ微塵もありません。

だから、私の行動がどのような背景に基づいているのか、ちゃんと観察できない人からみると、私には謙虚さは微塵もありません。


実際問題としては。

実は Act の途中から後、すでに若干謙虚じゃありません。
Act は「問題の発見と解決」です。観察の結果、問題が発見されるのが Act の前半です。

で、後半。ここで謙虚になるとはどういうことか??

俺には解けないかもしれない…

と引いてしまう事です。それでは解ける物だって解けない。

俺に解けないなら
誰にも解けんわいっ!!

というぐらいの気概で問題に当たらなければ、人生やってられません。

ちなみに。何事にも例外はあるもので。

私の場合、あまり もてませんな、この戦略で生きていると。

2009年7月22日

Linux kernel zero-day exploit

備忘録。

対象2.6.30, 2.6.30.1
真の原因null pointer check をしていないコード & gcc optimization
修正コード2.6.30.2
demohttp://www.youtube.com/watch?v=UdkpJ13e6Z0
解説しているページ
原因となっているパッチhttp://mirror.celinuxforum.org/gitstat/commit-detail.php?commit=33dccbb050bbe35b88ca8cf1228dcf3e4d4b3554


問題はこの部分。
@@ -461,7 +476,8 @@ static unsigned int tun_chr_poll(struct file *file, poll_table * wait)
{
struct tun_file *tfile = file->private_data;
struct tun_struct *tun = __tun_get(tfile);
- unsigned int mask = POLLOUT | POLLWRNORM;
+ struct sock *sk = tun->sk;
+ unsigned int mask = 0;

if (!tun)
return POLLERR;


まず、前提として理解しておくべきことは、C言語においては NULL Pointer へのアクセスの結果は「未定義」だということ。なのでコード的には何が起こっても不思議はない。当然、「未定義」以降に実施されるコードも未定義になる。

次に理解するべきことは、「未定義とはいえ、大抵のgccを利用する環境では NULL ポインタへのアクセスは SIGSEGV などを発生させる」事。つまり NULL ポインタアクセスを行えば、その段階でプログラムは停止するので、そこを通過したコードが持っているポインタは NULL ではない、と仮定して構わない事。どうせ、NULL だったら未定義なんだから何しても構わないし。結果として出力されるコードは論理的には必要な条件をすべて満たす結果として安全サイドに倒れたコードにはならない

以上のことを念頭において、先ほどのパッチを見る。
+ struct sock *sk = tun->sk;

このコードは tun が NULL だった場合、ユーザープログラムであれば SIGSEGV を発生させる。しかし、 kernel の場合はそのような割込の類は発生しない(正確にはしなかった)。

gcc は、「tun->sk を通過できたコードは、tun が NULL ではない、という保証が得られた、と言うことだ。その後 tun は変更されていない」と考えつつ、次のコードを見る。
  if (!tun)
return POLLERR;

で、続けてこう考えるわけだ。
「ふむ。ここまで来た段階で、tun != NULL は真であり、それ以降変更されていない。ならば
if (!tun) は常に偽だ。」
結果として上記のコードは全部なくなる。

結果として全体はこうなる:
static unsigned int tun_chr_poll(struct file *file, poll_table * wait)
{
struct tun_file *tfile = file->private_data;
struct tun_struct *tun = __tun_get(tfile);
struct sock *sk = tun->sk;
unsigned int mask = 0;

poll_wait(file, &tun->read_wait, wait);
 :

POLLERR を返すパスはなくなり、エラーチェックが消失する。


エラーチェックが無くなれば、今度はこっちの問題を使って NULL pointer 近くのページに予め mmap しておいたコードを実行できてしまう。なにしろ、ユーザー空間は全て kernel から参照、実行可能だから。



ちなみに、2.6.18 base のはずの RHEL5.4 beta にはこれらのバグが存在した(NULL pointer check 関係の最適化がバックポートされた)が、それぞれ修正されているそうだ。

2009年6月22日

A. 笑わない数学者の問題

まだ確定していない予想はここに書く。

3. 笑わない数学者の問題
2. 笑わない数学者の問題
笑わない数学者の問題

なんとなく見えてきた。これは Pn が問題児なんじゃない。

Pn を決めると、 P3 から Pn-1 までの値の範囲が決定する ( P1 と P2 は固定なので影響しない )。特に Pn-1 の値の範囲には強い影響力がある。

また、 Pn = ( n*(n-1)/2 - x ) の 「x の値」が大事。x の値に応じて、同じ Pnに対して、 2x 組のパターンが発生する。

で、x が決まったときに Pn の周りにどのように数字を配置できるのかを突き詰めていくと、x ごとに有効な n の範囲が出てくる。また、その n が成り立つための拘束条件も出てきて、Pn周りのボールの何個かが確定する。そこから先は n ごとの探索になるが、ここまでは「nとは関係なく」求められる。そういう問題なのだろう。

だから 「n を与えられたときに解を求める」という探索戦略を使うと、いったん x の範囲を求めることになり、その後各 x について解を探すことになるが、これは「与えられた n 以外についての解」を大量に探索することになる。その次の n を与えるとこれまた同じような x の範囲について同じような探索をする必要があり…ここが大量に存在する重複した計算部分なのだろう。これは x ごとに n を求めた look up table をまず用意して、次に n ごとにその lookup table を探索するべきなんだ。

2009年6月13日

んー、何がどうなっているのやら

www.ascii.jp はご存知 ascii.jp へのアドレスだ。
では、これは? www1.ascii.jp …うん。これも ascii.jp へのアドレスだ。

ではでは、ともちゃ日記 で指し示されていた www2.ascii.jp は? …ともちゃ さんご指摘のままだ…いいのか? これ…

ではではでは、 www3.ascii.jp は??!
Apache 2 Test Page
powered by CentOS


…おいっっ!!! いきなりかっ!!! いきなりテストページですかっっ!! インストールしたままのっ!!!!
それは、渋谷の町を 下着姿で歩いているのと一緒だっ!!!
とっとと上着を着ろっっ!!!!

ちなみに、www4.ascii.jp はエラーになった。よかったよかった。

2009年5月6日

RAWデータを操れるのか…UFRaw…こりゃ便利だ

デジカメとして ニコンのD70 と PanasonicのLUMIX-G1 を持っている。で、この2つのカメラ、どちらも RAW フォーマットをサポートしている。

サポートしているのはよいのだが、これを読み込むためのソフトがPhotoshop等の有料ソフトしかなかった。メーカーごとにフォーマットが違い、JPEGのように標準化されていないからだ。そのため、今まではJPEGで撮影していたのだが…

UFRawというソフトがある事を知った。各社のRawファイルを読み込み、ある程度の色調調整などは自分でもできるソフト。
最大のポイントはGIMPのプラグインとしても動作するので、RAWデータをGIMPで操作して、結果を別のフォーマットなどにも保存できることだろう。

G1はRawとJpegを同時保存できるので、それで見ていると、たまにバグって色調がおかしくなっている場合があるが、それ以外は快調。

今までは操作もできなければプレビューすらできないRawモードは避けてきたんだけれど、これからは使ってみようかと思う。

2009年4月11日

複数のクレジットカードを持つのは効果的なのか?

いま、あちこちの銀行の定期預金とかの利率を調べている。金額的には100万円未満の利率。期間的には3年以内。円建てに限定する。

一例としては、こんな感じ。

みずほ銀行 普通預金金利 0.040%
定期預金 0.15% - 0.25%
貯蓄預金 0.07%
三菱東京UFJ銀行 普通預金 年0.040%
スーパー普通預金 年0.050%
スーパー定期 年0.150% - 年0.250%
自由金利型定期預金(大口定期) 年0.150% - 年0.350%
貯蓄預金 年0.040% - 年0.050%


見ての通り、出し入れ自由だと 0.04% - 0.05%, 出し入れ不自由だと 0.15% - 0.35% と言った所。3年預けたとして、利子は

0.04%1.0004^3 = 1.00120048 0.12%
0.05%1.0005^3 = 1.00150075 0.15%
0.15%1.0015^3 = 1.00450675 0.45%
0.35%1.0035^3 = 1.010536791.05%

と言った所。



なぜこんなことを計算しているかと言うと、実はこの本を読んでいるから。

この「新・資本論」という本の中に、こういう記述があるのだ。

日本でお金を稼いだとき、私はそれを三つに分けることを推奨する。三分の一をドルに、三分の一をユーロに、そして残りを円建ての金融商品に転換するのである。(p.142)

為替相場が乱高下しているときに「三分の一」にするのはどうかと思うが、これは結構面白いポイントをついていると思う。つまりこういうことを考えるのだ:


普段から銀行口座を3種類持つ。ドル建て、円建て、ユーロ建ての3つ。
そして、それぞれに対してクレジットカードを1つづつ作る。
収入を得たら、日常に使うお金は円建てに、残りを為替相場に応じて、適当に分配する。


何か買い物をする場合は、そのときの為替相場を考慮して、もっとも効果的な口座から引き落とされるカードを利用する。



例えば円高ドル安ユーロ安の場合は、すべての決済を円口座カードで済ませる。また、ドルやユーロ建ての講座に多い目にお金を預ける。

逆に円安ドル高になったら、決済をドル口座カードで済ませる。この場合、円高ドル安だった頃に買ったドルで決済をすることになるので、為替差額がそのまま利子として働く。

このような運用をした場合と、すべてを円建てで運用して定期預金などを使った場合と、どっちがお徳か考えてみよう、というわけ。話を簡単にするために、期間は3年限定とする。また、為替相場は現在の1ドル100円を基準として、1ドル最低95円、最高105円の範囲で動くとしよう。ユーロは考えない。とりあえず面倒だから。最後に、カード破産はしない、という前提も必要だね。

ようするに、1ドル95-100円の範囲の場合、ドル建てを 60%, 円建てを40%で、100-105円の場合、ドル建てを40%, 円建てを60%で普通預金に入れる事を考える。どうせ利子は付かないに等しいので、考慮しない。

一方で、消費する場合は1ドル95-100円の範囲の場合は円建てカードを優先、100-105円の場合はドル建てカードを優先で使うことにする。不足したらもう一方のカードで支払う。

これをすべて円建てで見ると、円建て口座の方は入れた金額が貯蓄金額だ。一方でドル建て口座の方は、1ドル97.5円平均で入金して、1ドル102.5円平均で出金することになる。利子としては 5.12% ついているのに等しい。円とドル半々で利用するので、実質利子はこの半分、2.5%…さらに不足分が出た場合のソンを考慮すると… 2%ぐらいか。



なんだ。

各通貨の銀行預金を作り、それぞれを引き落とし先とするクレジットカードを作り、為替相場に応じてどのカードで引き落とすかを変える
という比較的リスクの少ない戦略を使っただけでも、銀行の定期預金よりも利率が良い結果になるじゃないか。

2009年3月7日

解答を書いておく -3- 部分解の2

request2


meminfo.awk が存在する、と言う前提からはじめる。

SLAB=$(( awk -f meminfo.awk /proc/meminfo >> $LOGFILE ) 2>&1 )

で $SLAB に現在の /proc/meminfo 中のSLABの値が取得できているとしよう。他に次のような値を bash スクリプトレベルで取得、定義できているとする。

# We only have 876Mbytes of memory space for kernel.
KERNELMEMORY=$( expr 876*1024 )
PREVIOUSSTATE="GREEN"

# 358809kbyte is 40% of 876*1024Kbytes
YELLOWBORDERSIZE=358809

# or define this.
# YELLOWLIMIT=0.4

# 627916kbyte is 70% of 876*1024Kbytes
REDBORDERSIZE=627916

# or define this.
# REDLIMIT=0.7

KERNELMEMORY はコメントにあるとおり。32bit モードの Linux は 876Mbyteしかカーネル用空間が無いのでこれが利用できる最大値。

PREVIOUSSTATEは「前回計算した際のステート」。ステートは GREEN, YELLOW, RED のどれかしかないとし、全部文字列で表すことにする。

YELLOWBORDERSIZE は Slab がこの値を超えていたらステートを YELLOW として出力する値。
YELLOWLIMIT は KERNELMEMORY サイズにこの値を乗じて得られた結果を YELLOWBORDERSIZE として用いる、という値。
もし、YELLOWLIMIT と YELLOWBORDERSIZE の両方が定義されていたらどちらか小さい方を採択する。
片方がそんざいしてもう一方が存在しないなら、存在する方が有効になる。どちらも存在しなければ YELLOWBORDERSIZE=358809 が定義されているものとして処理する。

REDBORDERSIZE は Slab がこの値を超えていたらステートを RED として出力する値。
REDLIMIT は KERNELMEMORY サイズにこの値を乗じて得られた結果を REDBORDERSIZE として用いる、という値。
もし、REDLIMIT と REDBORDERSIZE の両方が定義されていたらどちらか小さい方を採択する。
片方がそんざいしてもう一方が存在しないなら、存在する方が有効になる。どちらも存在しなければ REDBORDERSIZE=627916 が定義されているものとして処理する。

もし、YELLOWLIMIT や YELLOWBORDERSIZE が何らかの理由で REDLIMIT や REDBORDERSIZE よりも大きい場合は、自動的に REDLIMIT, REDBORDERSIZE と同じ値にまで下がったものとみなす。

何らかの理由でREDLIMIT, REDBORDERSIZE が KERNELMEMORY 以上の値になった場合は、REDBORDERSIZE=$KERNELMEMORY と見なす。これが意味のある制約かどうかは微妙だが…。

YELLOWLIMIT, YELLOWBORDERSIZE が 0以下の場合はデフォルト値が採択される。

REDLIMIT, REDBORDERSIZE が 0以下の場合はデフォルト値が採択される。



これで判るとおり、実は境界問題はかなり厄介な問題をはらんでいる。与える境界値が適切な場合は特に問題ないのだが、境界値が異常値の場合、妥当に動かすのは著しく困難を伴う。その最大の理由は、プログラマが妥当な状態を決めなくてはいけないからだ。

この問題の本当の難しいところは、条件判断ではない。

妥当ではない計算条件を与えられたときに、それをいかにして跳ね除けるか、という点だ。



さて、プログラムだ。浮動小数点演算を bash で実行するのは難しいので、awk で実装する。いくつかの定数については、awk側に埋め込む。残りの引数は gawk の -v オプションを用いて引き渡す。

CalculateStatus.awk
#! /bin/gawk -f
BEGIN {
KERNELMEMORY = 876 * 1024;
DefaultYELLOWBORDERSIZE = 358809;
DefaultREDBORDERSIZE = 627916;
GreenName = "GREEN";
YellowName = "YELLOW";
RedName = "RED";


# First, calucate PreYELLOWBORDERSIZE without thinking about RED.
if ( YELLOWLIMIT > 0 ) {
PreYELLOWBORDERSIZE = KERNELMEMORY * YELLOWLIMIT;
} else {
PreYELLOWBORDERSIZE = 0;
}
if ( YELLOWBORDERSIZE > 0 ) {
if ( PreYELLOWBORDERSIZE > 0 ) {
if ( PreYELLOWBORDERSIZE > YELLOWBORDERSIZE ) {
PreYELLOWBORDERSIZE = YELLOWBORDERSIZE;
}
} else {
PreYELLOWBORDERSIZE = YELLOWBORDERSIZE;
}
} else {
PreYELLOWBORDERSIZE = DefaultYELLOWBORDERSIZE;
}

# First, calucate PreREDBORDERSIZE without thinking about YELLOW.
if ( REDLIMIT > 0 ) {
PreREDBORDERSIZE = KERNELMEMORY * REDLIMIT;
} else {
PreREDBORDERSIZE = 0;
}
if ( REDBORDERSIZE > 0 ) {
if ( PreREDBORDERSIZE > 0 ) {
if ( PreREDBORDERSIZE > REDBORDERSIZE ) {
PreREDBORDERSIZE = REDBORDERSIZE;
}
} else {
PreREDBORDERSIZE = rEDBORDERSIZE;
}
} else {
PreREDBORDERSIZE = DefaultREDBORDERSIZE;
}

# OK. Re-calculate YELLOW according to RED.
if ( PreREDBORDERSIZE < PreYELLOWBORDERSIZE ) {
PreYELLOWBORDERSIZE = PreREDBORDERSIZE;
}

YELLOWBORDERSIZE = PreYELLOWBORDERSIZE;
REDBORDERSIZE = PreREDBORDERSIZE;

if ( SLAB > REDBORDERSIZE ) {
NewStatus = RedName;
} else if ( Slab > YELLOWBORDERSIZE ) {
NewStatus = YellowName;
} else {
NewStatus = GreenName;
}

# Ok. check old status in mind.
if ( PREVIOUSSTATE == RedName ) {
NewStatus = RedName;
} else if (( PREVIOUSSTATE == YellowName )&&( NewStatus = GreenName )) {
NewStatus = YellowName;
}

print NewStatus;
exit 0;
}

結果は stdout で出て行くので、bash 変数に取り込めばよい。

2009年3月1日

解答を書いておく -2- 部分解

まずは request3 から


request3 は比較的簡単だ。sed でも awk でもできる。

sed ならばこう:
sed 's/^/# /g' /proc/slabinfo


awk ならばこう:
awk '{ print "# " $0; }' /proc/slabinfo


awk の場合は正規表現を必要としない分早いかもしれない。でもそもそも sed の方がプログラムとして小さいのでこちらの方が早いかもしれない。どちらがいいのかはよく判らない。

つぎに request1


/proc/meminfo の各行から数字の部分を切り出すだけなら
awk '{ print $2; }' /proc/meminfo

だけで済む。しかしここには悪魔が潜んでいる。

難しいポイントは3つ。

  1. 時刻を出力する

  2. /proc/meminfo の中身の順序がカーネル依存である

  3. /proc/meminfo の Slab フィールドの値は、request2 でも使う



時刻を出力するには、最初に date 出力を getline で取得すればよい。
BEGIN {
"date +"%Y/%m/%d %H:%M:%S" | getline DATEVAL;
DATETAG="date";
}


/proc/meminfo の中身自体は適当なバージョンにあわせるとしよう。':' よりも左側の文字列を取ってくるには:
sed 's/^\(.*\):.*$/\1/g' /proc/meminfo

とやればよい。結果として
MemTotal
MemFree
Buffers
Cached
SwapCached
:
のような出力が得られる。これをベースに、<各エントリ名>TAG という変数と、<各エントリ名>VALという変数を作ろう。こんな感じだ:
for i in $(sed 's/^\(.*\):.*$/\1/g' /proc/meminfo ); do
echo "/^${i}:/ { ${i}VAL = \$2; ${i}TAG=\"${i}\"; }"
done

結果はこんな感じになる。
/^MemTotal:/ { MemTotalVAL = $2; MemTotalTAG="MemTotal"; }
/^MemFree:/ { MemFreeVAL = $2; MemFreeTAG="MemFree"; }
/^Buffers:/ { BuffersVAL = $2; BuffersTAG="Buffers"; }
/^Cached:/ { CachedVAL = $2; CachedTAG="Cached"; }
/^SwapCached:/ { SwapCachedVAL = $2; SwapCachedTAG="SwapCached"; }
:

各行が awk スクリプトにおける「対応する値をとってくるためのスクリプト」になっている。
同様にして、出力するコードを吐かせる。
for i in $(sed 's/^\(.*\):.*$/\1/g' meminfo ); do
echo " printf(\"\\\"\" ${i}VAL \"\\\"\\t\" );"
done
結果はこんな感じ。
 printf("\"" MemTotalVAL "\"\t" );
printf("\"" MemFreeVAL "\"\t" );
printf("\"" BuffersVAL "\"\t" );
printf("\"" CachedVAL "\"\t" );
printf("\"" SwapCachedVAL "\"\t" );
:
あと、「VAL」の部分を「TAG」に変えたものも用意する。

TAGをいつ出力するのか、その制御は bash 側からすることにしよう。PRINTTAGという変数を用意して、これが 0 だったら TAG を出さない。それ以外だったら出す。

printf の最後の要素だけは \t を \n に直す。また、date を出力するコードを加える。

以上を全部行うとこうなる:
BEGIN {
"date +'%Y/%m/%d %H:%M:%S'" | getline DATEVAL;
DATETAG="date";
}

/^MemTotal:/ { MemTotalVAL = $2; MemTotalTAG="MemTotal"; }
/^MemFree:/ { MemFreeVAL = $2; MemFreeTAG="MemFree"; }
/^Buffers:/ { BuffersVAL = $2; BuffersTAG="Buffers"; }
/^Cached:/ { CachedVAL = $2; CachedTAG="Cached"; }
/^SwapCached:/ { SwapCachedVAL = $2; SwapCachedTAG="SwapCached"; }
/^Active:/ { ActiveVAL = $2; ActiveTAG="Active"; }
/^Inactive:/ { InactiveVAL = $2; InactiveTAG="Inactive"; }
/^HighTotal:/ { HighTotalVAL = $2; HighTotalTAG="HighTotal"; }
/^HighFree:/ { HighFreeVAL = $2; HighFreeTAG="HighFree"; }
/^LowTotal:/ { LowTotalVAL = $2; LowTotalTAG="LowTotal"; }
/^LowFree:/ { LowFreeVAL = $2; LowFreeTAG="LowFree"; }
/^SwapTotal:/ { SwapTotalVAL = $2; SwapTotalTAG="SwapTotal"; }
/^SwapFree:/ { SwapFreeVAL = $2; SwapFreeTAG="SwapFree"; }
/^Dirty:/ { DirtyVAL = $2; DirtyTAG="Dirty"; }
/^Writeback:/ { WritebackVAL = $2; WritebackTAG="Writeback"; }
/^Mapped:/ { MappedVAL = $2; MappedTAG="Mapped"; }
/^Slab:/ { SlabVAL = $2; SlabTAG="Slab"; }
/^CommitLimit:/ { CommitLimitVAL = $2; CommitLimitTAG="CommitLimit"; }
/^Committed_AS:/ { Committed_ASVAL = $2; Committed_ASTAG="Committed_AS"; }
/^PageTables:/ { PageTablesVAL = $2; PageTablesTAG="PageTables"; }
/^VmallocTotal:/ { VmallocTotalVAL = $2; VmallocTotalTAG="VmallocTotal"; }
/^VmallocUsed:/ { VmallocUsedVAL = $2; VmallocUsedTAG="VmallocUsed"; }
/^VmallocChunk:/ { VmallocChunkVAL = $2; VmallocChunkTAG="VmallocChunk"; }
/^HugePages_Total:/ { HugePages_TotalVAL = $2; HugePages_TotalTAG="HugePages_Total"; }
/^HugePages_Free:/ { HugePages_FreeVAL = $2; HugePages_FreeTAG="HugePages_Free"; }
/^Hugepagesize:/ { HugepagesizeVAL = $2; HugepagesizeTAG="Hugepagesize"; }

END {
if ( PRINTTAG != 0 ) {
printf("\"" DATETAG "\"\t" );
printf("\"" MemTotalTAG "\"\t" );
printf("\"" MemFreeTAG "\"\t" );
printf("\"" BuffersTAG "\"\t" );
printf("\"" CachedTAG "\"\t" );
printf("\"" SwapCachedTAG "\"\t" );
printf("\"" ActiveTAG "\"\t" );
printf("\"" InactiveTAG "\"\t" );
printf("\"" HighTotalTAG "\"\t" );
printf("\"" HighFreeTAG "\"\t" );
printf("\"" LowTotalTAG "\"\t" );
printf("\"" LowFreeTAG "\"\t" );
printf("\"" SwapTotalTAG "\"\t" );
printf("\"" SwapFreeTAG "\"\t" );
printf("\"" DirtyTAG "\"\t" );
printf("\"" WritebackTAG "\"\t" );
printf("\"" MappedTAG "\"\t" );
printf("\"" SlabTAG "\"\t" );
printf("\"" CommitLimitTAG "\"\t" );
printf("\"" Committed_ASTAG "\"\t" );
printf("\"" PageTablesTAG "\"\t" );
printf("\"" VmallocTotalTAG "\"\t" );
printf("\"" VmallocUsedTAG "\"\t" );
printf("\"" VmallocChunkTAG "\"\t" );
printf("\"" HugePages_TotalTAG "\"\t" );
printf("\"" HugePages_FreeTAG "\"\t" );
printf("\"" HugepagesizeTAG "\"\n" );
}

printf("\"" DATEVAL "\"\t" );
printf("\"" MemTotalVAL "\"\t" );
printf("\"" MemFreeVAL "\"\t" );
printf("\"" BuffersVAL "\"\t" );
printf("\"" CachedVAL "\"\t" );
printf("\"" SwapCachedVAL "\"\t" );
printf("\"" ActiveVAL "\"\t" );
printf("\"" InactiveVAL "\"\t" );
printf("\"" HighTotalVAL "\"\t" );
printf("\"" HighFreeVAL "\"\t" );
printf("\"" LowTotalVAL "\"\t" );
printf("\"" LowFreeVAL "\"\t" );
printf("\"" SwapTotalVAL "\"\t" );
printf("\"" SwapFreeVAL "\"\t" );
printf("\"" DirtyVAL "\"\t" );
printf("\"" WritebackVAL "\"\t" );
printf("\"" MappedVAL "\"\t" );
printf("\"" SlabVAL "\"\t" );
printf("\"" CommitLimitVAL "\"\t" );
printf("\"" Committed_ASVAL "\"\t" );
printf("\"" PageTablesVAL "\"\t" );
printf("\"" VmallocTotalVAL "\"\t" );
printf("\"" VmallocUsedVAL "\"\t" );
printf("\"" VmallocChunkVAL "\"\t" );
printf("\"" HugePages_TotalVAL "\"\t" );
printf("\"" HugePages_FreeVAL "\"\t" );
printf("\"" HugepagesizeVAL "\"\n" );
}

このスクリプト全体を、meminfo1.awk として保存する。で、/proc/meminfo を食わせてみるとこんな感じになる。
$ gawk -v PRINTTAG=1 -f meminfo1.awk /proc/meminfo 
"date" "MemTotal" "MemFree" "Buffers" "Cached" "SwapCached" "Active" "Inactive" "HighTotal" "HighFree" "LowTotal" "LowFree" "SwapTotal" "SwapFree" "Dirty" "Writeback" "Mapped" "Slab" "CommitLimit" "Committed_AS" "PageTables" "VmallocTotal" "VmallocUsed" "VmallocChunk" "HugePages_Total" "HugePages_Free" "Hugepagesize"
"2009/03/01 23:00:00" "1001008" "200708" "43400" "395740" "0" "557556" "178624" "97216" "140" "903792" "200568" "2096472" "2096472" "224" "0" "356492" "47820" "2596976" "689048" "6192" "114680" "4560" "107264" "0" "0" "2048"
$ gawk -v PRINTTAG=0 -f meminfo1.awk /proc/meminfo
"2009/03/01 23:00:37" "1001008" "200708" "43400" "395740" "0" "557556" "178624" "97216" "140" "903792" "200568" "2096472" "2096472" "224" "0" "356492" "47820" "2596976" "689048" "6192" "114680" "4560" "107264" "0" "0" "2048"
$ gawk -f meminfo1.awk /proc/meminfo
"2009/03/01 23:00:37" "1001008" "200708" "43400" "395740" "0" "557556" "178624" "97216" "140" "903792" "200568" "2096472" "2096472" "224" "0" "356492" "47820" "2596976" "689048" "6192" "114680" "4560" "107264" "0" "0" "2048"

-v を使って PRINTTAG 変数をスクリプト外部から制御した。最後の -v を指定しない例は、ようするに「デフォルトの挙動」と言う奴になる。

やれこれで一安心…ではない。Slab の値だけ、別途別パスで外に出してやる必要がある。幸い、stderr という文明の利器があるのでこれを使う。
    print SlabVAL > "/dev/stderr";

これを END {} セクションの最後に加えるのだ。そうすると Slabの数字の部分だけが stderr で取り出せる。

request1に対する最終 meminfo.awk スクリプトはこうなる:
BEGIN {
"date +'%Y/%m/%d %H:%M:%S'" | getline DATEVAL;
DATETAG="date";
}

/^MemTotal:/ { MemTotalVAL = $2; MemTotalTAG="MemTotal"; }
/^MemFree:/ { MemFreeVAL = $2; MemFreeTAG="MemFree"; }
/^Buffers:/ { BuffersVAL = $2; BuffersTAG="Buffers"; }
/^Cached:/ { CachedVAL = $2; CachedTAG="Cached"; }
/^SwapCached:/ { SwapCachedVAL = $2; SwapCachedTAG="SwapCached"; }
/^Active:/ { ActiveVAL = $2; ActiveTAG="Active"; }
/^Inactive:/ { InactiveVAL = $2; InactiveTAG="Inactive"; }
/^HighTotal:/ { HighTotalVAL = $2; HighTotalTAG="HighTotal"; }
/^HighFree:/ { HighFreeVAL = $2; HighFreeTAG="HighFree"; }
/^LowTotal:/ { LowTotalVAL = $2; LowTotalTAG="LowTotal"; }
/^LowFree:/ { LowFreeVAL = $2; LowFreeTAG="LowFree"; }
/^SwapTotal:/ { SwapTotalVAL = $2; SwapTotalTAG="SwapTotal"; }
/^SwapFree:/ { SwapFreeVAL = $2; SwapFreeTAG="SwapFree"; }
/^Dirty:/ { DirtyVAL = $2; DirtyTAG="Dirty"; }
/^Writeback:/ { WritebackVAL = $2; WritebackTAG="Writeback"; }
/^Mapped:/ { MappedVAL = $2; MappedTAG="Mapped"; }
/^Slab:/ { SlabVAL = $2; SlabTAG="Slab"; }
/^CommitLimit:/ { CommitLimitVAL = $2; CommitLimitTAG="CommitLimit"; }
/^Committed_AS:/ { Committed_ASVAL = $2; Committed_ASTAG="Committed_AS"; }
/^PageTables:/ { PageTablesVAL = $2; PageTablesTAG="PageTables"; }
/^VmallocTotal:/ { VmallocTotalVAL = $2; VmallocTotalTAG="VmallocTotal"; }
/^VmallocUsed:/ { VmallocUsedVAL = $2; VmallocUsedTAG="VmallocUsed"; }
/^VmallocChunk:/ { VmallocChunkVAL = $2; VmallocChunkTAG="VmallocChunk"; }
/^HugePages_Total:/ { HugePages_TotalVAL = $2; HugePages_TotalTAG="HugePages_Total"; }
/^HugePages_Free:/ { HugePages_FreeVAL = $2; HugePages_FreeTAG="HugePages_Free"; }
/^Hugepagesize:/ { HugepagesizeVAL = $2; HugepagesizeTAG="Hugepagesize"; }

END {
if ( PRINTTAG != 0 ) {
printf("\"" DATETAG "\"\t" );
printf("\"" MemTotalTAG "\"\t" );
printf("\"" MemFreeTAG "\"\t" );
printf("\"" BuffersTAG "\"\t" );
printf("\"" CachedTAG "\"\t" );
printf("\"" SwapCachedTAG "\"\t" );
printf("\"" ActiveTAG "\"\t" );
printf("\"" InactiveTAG "\"\t" );
printf("\"" HighTotalTAG "\"\t" );
printf("\"" HighFreeTAG "\"\t" );
printf("\"" LowTotalTAG "\"\t" );
printf("\"" LowFreeTAG "\"\t" );
printf("\"" SwapTotalTAG "\"\t" );
printf("\"" SwapFreeTAG "\"\t" );
printf("\"" DirtyTAG "\"\t" );
printf("\"" WritebackTAG "\"\t" );
printf("\"" MappedTAG "\"\t" );
printf("\"" SlabTAG "\"\t" );
printf("\"" CommitLimitTAG "\"\t" );
printf("\"" Committed_ASTAG "\"\t" );
printf("\"" PageTablesTAG "\"\t" );
printf("\"" VmallocTotalTAG "\"\t" );
printf("\"" VmallocUsedTAG "\"\t" );
printf("\"" VmallocChunkTAG "\"\t" );
printf("\"" HugePages_TotalTAG "\"\t" );
printf("\"" HugePages_FreeTAG "\"\t" );
printf("\"" HugepagesizeTAG "\"\n" );
}

printf("\"" DATEVAL "\"\t" );
printf("\"" MemTotalVAL "\"\t" );
printf("\"" MemFreeVAL "\"\t" );
printf("\"" BuffersVAL "\"\t" );
printf("\"" CachedVAL "\"\t" );
printf("\"" SwapCachedVAL "\"\t" );
printf("\"" ActiveVAL "\"\t" );
printf("\"" InactiveVAL "\"\t" );
printf("\"" HighTotalVAL "\"\t" );
printf("\"" HighFreeVAL "\"\t" );
printf("\"" LowTotalVAL "\"\t" );
printf("\"" LowFreeVAL "\"\t" );
printf("\"" SwapTotalVAL "\"\t" );
printf("\"" SwapFreeVAL "\"\t" );
printf("\"" DirtyVAL "\"\t" );
printf("\"" WritebackVAL "\"\t" );
printf("\"" MappedVAL "\"\t" );
printf("\"" SlabVAL "\"\t" );
printf("\"" CommitLimitVAL "\"\t" );
printf("\"" Committed_ASVAL "\"\t" );
printf("\"" PageTablesVAL "\"\t" );
printf("\"" VmallocTotalVAL "\"\t" );
printf("\"" VmallocUsedVAL "\"\t" );
printf("\"" VmallocChunkVAL "\"\t" );
printf("\"" HugePages_TotalVAL "\"\t" );
printf("\"" HugePages_FreeVAL "\"\t" );
printf("\"" HugepagesizeVAL "\"\n" );

print SlabVAL > "/dev/stderr";
}

STDOUT はログファイルに出力させる。その後 STDERR を stdout にリダイレクトして、変数に食わせてやる。実行する側のシェルはこんな感じ:
SLAB=$(( awk -f meminfo.awk /proc/meminfo >> $LOGFILE ) 2>&1 )

これで SLAB 変数には Slab: の数字のフィールドが入り、かつ全部の出力が $LOGFILE に出力される。

解答を書いておく -1- 課題

仕事で、ある人にスクリプト作成を依頼している。
内容的には単純なのだが、ちょっとややこしい部分があって、論理的に考えないと答が出ないようになっている。半分、演習問題のようなテーマだ。

順調に遅延しているのはいいのだが、私自身他に3件も受け持っているので、
「いざ、もう間に合いません」
と言うときになってからスクリプトを書き始めると、ちょっと間に合わなかったりする。なので、ここに模範解答の形で答を書いておこうか、と思う。いざとなったらここを参照すればよいわけだ。

他の人の演習問題にもなったらな、と思うので、問題も全部公開しよう。秘密といえるような部分は一切無い。



課題


request1


/proc/meminfo の内容を定期的に参照して欲しい。/proc/meminfo のデータは1項目一行形式になっているので、これを1行の TSV に直して欲しい。最初の一回だけ、どの項目がどこにあるのかを示すラベルもつけて欲しい。また、最初の要素に、/proc/meminfo の情報をいつ取得したのか、date の情報を "YYYY/MM/DD hh:mm:ss" の形式で保存して欲しい。

request2


/proc/meminfo には Slab という項目がある。ここの値がある閾値を超えたら、定期的に参照するその参照頻度を上げて欲しい。ようするにデータを取る間隔を短くして欲しいのだ。問題は、いったん間隔を短くしたら、二度と長くしないで欲しい、と言うことと、「閾値」は2つ、Yellow と Red があるので、それぞれに応じて間隔を短くして欲しい。また、この閾値はあとで簡単に変更できるように、スクリプトの上のほうの行で名前で宣言して欲しい。もちろん、その際の「間隔」もそれぞれ名前で宣言しておいて欲しい。

request3


Yellow の閾値を超えたら /proc/slabinfo のデータも取ってきて欲しい。こちらは、/proc/slabinfo のファイルの情報の各行の前に "# " (シャープがきて、その次に空白が1つ) をつけた形で、/proc/meminfo のデータを保存しているログファイルと同じ所に保存して欲しい。

課題の詳細


request1


/proc/meminfo を読んでくると次のようなフォーマットで値が出力される。
MemTotal:      1001008 kB
MemFree: 200708 kB
Buffers: 43400 kB
Cached: 395740 kB
SwapCached: 0 kB
Active: 557556 kB
Inactive: 178624 kB
HighTotal: 97216 kB
HighFree: 140 kB
LowTotal: 903792 kB
LowFree: 200568 kB
SwapTotal: 2096472 kB
SwapFree: 2096472 kB
Dirty: 224 kB
Writeback: 0 kB
Mapped: 356492 kB
Slab: 47820 kB
CommitLimit: 2596976 kB
Committed_AS: 689048 kB
PageTables: 6192 kB
VmallocTotal: 114680 kB
VmallocUsed: 4560 kB
VmallocChunk: 107264 kB
HugePages_Total: 0
HugePages_Free: 0
Hugepagesize: 2048 kB

request 1 はようするにこれを

"1001008"<tab>"200708"<tab>"43400"....

のように1行に直して欲しい、という事だ。ただし、<tab> の部分は実際にはタブ文字コード (awk とかだと "\t")で分けて欲しい。また、数字は " で囲ってもらえると助かる。

また、最初の1回だけラベルの部分も欲しい。これはつまり
"MemTotal"<tab>"MemFree"<tab>"Buffers"....

のような行を出して欲しい、と言うこと。

さらに、一番最初にデータを取得した情報が欲しい、とある。これはようするに
date +"%Y/%m/%d %H:%M:%S"

の出力を取り込んで、最初に表示して欲しい、という事だ。当然ラベル行も実際には:
"date"<tab>"MemTotal"<tab>"MemFree"<tab>"Buffers"....

でなくては困る。

request2


/proc/meminfo のSlab:の項目が 358809 未満を Green,
358809 以上 627916 未満を Yellow,
627916 以上を Red
という状態とする。これは32bit Linux Kernel のメモリ領域が 876Mbyte しかなく、それぞれ 40%、70% の所に閾値を用意した、という事だ。実際には Slab が Yellow になることすらめったに無いはずで、Red に突入したら、間違いなく oom-killer が近々発動するだろう。その傾向を見たいのだ。

状態が Green の間は 300秒(5分)間隔で request1 を実施して欲しい。
Yellow になったら 60秒(1分)間隔で request1 と request3 を実施して欲しい。
Red になったら 30秒間隔で request1 と request3 を実施して欲しい。

いったん Yellow になったら、Slab の値が改善しても Green には戻らないで欲しい。同様に Red になったら、Yellow, Green には戻ってはいけない。なので、「今回の Slab の値だけで、次にデータ取得するまでの間隔を決める」のでは困る。

request3


cat /proc/slabinfo を実施するとこんな感じの出力が得られる。
slabinfo - version: 1.1
kmem_cache 60 78 100 2 2 1
blkdev_requests 5120 5120 96 128 128 1
mnt_cache 20 40 96 1 1 1
inode_cache 7005 14792 480 1598 1849 1
dentry_cache 5469 5880 128 183 196 1
filp 726 760 96 19 19 1
buffer_head 67131 71240 96 1776 1781 1
vm_area_struct 1204 1652 64 23 28 1

これの頭に "# " をつけて
# slabinfo - version: 1.1
# kmem_cache 60 78 100 2 2 1
# blkdev_requests 5120 5120 96 128 128 1
# mnt_cache 20 40 96 1 1 1
# inode_cache 7005 14792 480 1598 1849 1
# dentry_cache 5469 5880 128 183 196 1
# filp 726 760 96 19 19 1
# buffer_head 67131 71240 96 1776 1781 1
# vm_area_struct 1204 1652 64 23 28 1

としてほしいわけだ。これは、/proc/meminfo の次の行から出力して欲しい。だから:
"2009/03/01 10:48:58"<tab>"1001008"<tab>"200708"<tab>"43400"....
# slabinfo - version: 1.1
# kmem_cache 60 78 100 2 2 1
# blkdev_requests 5120 5120 96 128 128 1
# mnt_cache 20 40 96 1 1 1
# inode_cache 7005 14792 480 1598 1849 1
# dentry_cache 5469 5880 128 183 196 1
# filp 726 760 96 19 19 1
# buffer_head 67131 71240 96 1776 1781 1
# vm_area_struct 1204 1652 64 23 28 1

:

のような出力が延々続くログファイルが欲しい、という事になる。

背景


oom-killer が発動したシステムがあった。
/var/log/messages に残る oom-killer の行動痕跡によると、slab が異常に大きい。
そこで、slab の状態を記録すると同時に、自分たちが何をやっていたのかを記録する。

slab がでかくなったタイミングで何をやっていたのか知れば、問題点がわかるに違いない。そのための監視用ログシステムが欲しい、と言うわけ。

参考資料


スクリプト言語であれば何を使っても良かったのだが、とりあえず bash と sed と awk ということなので、参考資料は次の通り。

2009年2月13日

似たようなことがしてみたい人へ

えー、一応。似たようなことをしたい人がいる可能性を考えて、参考資料を。まずは上司/スポンサー説得資料
基本的にこれらの本に書いてあるのとやっていることは同じである、と主張しても大きく嘘にはならないと思います。いや、本当に同じ様なことをしているんですけれどね。統計学を仕事に応用しているんだし。ただ、あまりすさまじくかっこいい具合にありえないようなものを発見できる、と言う風に誤解されるとそれはそれで後が大変でしょうから、こう…加減と言うものは各自で調整してください。

で、これで何を手に入れるかと言うと、もちろん「計算機」です。大事なのは「64bit」と「メモリがたくさん」。いまどきのx86マシンならば EM64T対応な段階で SSEとかも相応に早いです。もちろん、Core2Duo とか Core2Quad とか Corei7 とかがいいわけですが、動作クロックは最低でも構わないかと。むしろ大事なのはメモリ。

Windows はやめておいた方がよいです。メモリの利用効率が悪い、あれは。ファイルキャッシュのデザインが unix 以前(1969年以前、と言うこと)のOSそっくりだ。計算用データを保持するために1pageでも余計にメモリを使いたいときに、ガッツリ kernel 内部のファイルキャッシュがページを握って離さない、というWindows NT/Longhorn デザインは勘弁して欲しい(つーか Windows7 では是非その辺りも改善していただきたいものだ)。と言うわけで、とりあえずお勧めは Linux 。マルチコア、HT対応、Intel & AMD 御謹製のプロセッサコントロールコードが使えるのは Linux ぐらいですから。

次に。あなた自身のための入門本。
多分、この一冊と Wikipedia 辺りが入門には丁度良いのではないかと。もちろん、前出の2冊も読んでいただくのですがあれは「どういう場面で統計を使うか」なのに対して、ここからは「その場面で何を使うか」。

で、最後に「なんとなく判ったけど、コーディングが面倒だな」とか「もっと詳しく知りたいが、できれば数学的なほうじゃなくて…」という人のための本。
多分これぐらいあれば、1年はここに書いてある内容で遊べます。

えぇ、「遊んで暮らせます」というオチだったらよかったんですけどねぇ。残念ながらそこまでは…

2009年2月11日

リファクタリング-5-

さて。一番重たい部分を計算するプログラム公開の前に。どうやら問題を理解してもらえていないようなので、もう一度書いておく。

m 個のデータ列がある。各データ列は N 個のデータからなる。N個のデータはm個のデータ列の間で互いにどれとどれが対応するのかは判っているとする。

これらのデータ列同士の相関を「総当りで」求めたい。相関係数 r は r(x,y) と r(y,x) (ただし、x, y はデータ列) は同じ値になるので、m個のデータ列同士の総当たり戦は m*(m-1)/2 通りになる。これを
面倒なコーディングを可能な限り行わない
形で、実用的な速度で解きたい。

R であれ、ライブラリであれ、モジュールであれ、1組2個のデータ列同士の相関を求める関数は存在するし、それはどれを使っても相応に早い。しかし「総当たり戦」の場合、その関数を m*(m-1)/2 回呼び出さなくてはいけない。

相関を求める式には、データ列単位で計算しておけばそれを使いまわせるものが結構ある。平均値を求めたり、各データと当該平均値との差分を求めたり、それら差分の二乗値の和の平方根を取ったりした結果などがそうだ。
ある任意のデータ列は、(m-1)/2 回相関を求めるために呼び出される。と言うことは、上記の値は、最初に1回だけ計算しておけば ((m-1)/2) - 1 回分、計算をしなくてよい。

このような最適化を施したライブラリは、いまだに存在しない。というか、存在しなくは無いのだが、メモリを食いすぎるため、私のPCでは遅すぎるのだ(32bit環境だと、そもそもアドレス空間が足りなくて core 吐いて終わる。64bit 環境は、メモリが足りないため swap IO ばかりで計算が全然前に進まない)。計算環境が64bit環境だけで作れて、メモリが潤沢にあれば問題は無いのだが…

その一方で。この相関は何のためにとるのかと言うと、「Windows の perfmon の出力から、何か性能劣化の兆候が見えないか?」とか「性能が劣化しているのだが、何が原因かわからないか?」という分析をする際の、調査対象に優先度をつけるために行っている。つまり計算誤差なんぞ多少あっても構わないのだ。まじめな数値計算ではなく、誤差が多少あってもいいから高速に結果を出して欲しい。

実用になる範囲であれば、perl 辺りでライブラリを叩けば十分だったのだが、速度が遅すぎる以上 C でコーディングをするしかないわけだ。

ちなみに、mは 27000.Nが 675。 perl で組んでいたときは、1組当たり 9msec かかっていた。つまり37.9673.. 38日かかる計算になる。これが C だと 37分…まぁIOの関係で40分ぐらい。「値が変化しない」とわかりきっている部分が半分近くあり、それらについては相関は絶対 NaN になると判っているのでメモリも消費しなくなった。すると32bit環境でも一発で計算できるではないか…。NIH(Not Invented Here)症候群と言われようが、自分でコーディングする気になるというものだよ。


次は一番重たい部分を計算するCのプログラム

correlation.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <math.h>

#define DEBUG (0)



typedef struct __data {
struct __data *next;
struct __data *prev;
char *element_name;
int correlation;
double sum;
double mean;
double sqsum;
double subpart;
long numdata;
double *datalist;
} data;


data* read_each( char *listfilename );
data* read_datafile( char *linebuffer );
void add_data_array( data **datalistpp, data *new_datap );
void calculate( data *data_list );
void remove_trailing_newline( char *linebuffer );

void help( char *programname );
void *o_malloc( size_t size, const char *msg, const char *func, const long line );



int
main( int argc, char *argv[] )
{
data *data_list;
if ( argc != 2 ) {
help( argv[0] );
}

data_list = read_each( argv[1] );
if ( data_list == NULL ) {
fprintf( stderr, "no data given.\n" );
exit( 1 );
}

calculate( data_list );
}

data*
read_each( char *listfilename )
{
FILE *fp;
char *linebuffer;
int linebufsize;
data *read_datap = NULL;


linebufsize = getpagesize();
linebuffer = o_malloc( linebufsize, "unable to prepare linebuffer\n", __func__, __LINE__ );

fp = fopen( listfilename, "r" );
if ( fp == NULL ) {
fprintf( stderr, "unable to open file %s\n", listfilename );
exit( 1 );
}

while( fgets( linebuffer, linebufsize, fp ) != NULL ) {
data *new_datap;
long len;

if ( linebuffer[0] == '#' ) {
continue;
}

remove_trailing_newline( linebuffer );

new_datap = read_datafile( linebuffer );
if ( new_datap->correlation != 0 ) {
add_data_array( &read_datap, new_datap );
} else {
/* take it away from the memory.*/
free( new_datap->datalist );
free( new_datap->element_name );
free( new_datap );
}
}
fclose( fp );

return read_datap;
}

data*
read_datafile( char *datafilename )
{
data *newdata;
FILE *fp;
char *linebuffer;
size_t linebufsize;
char *tagname;


newdata = o_malloc( sizeof( data ), "unable to allocate memory for newdata\n", __func__, __LINE__ );

linebufsize = getpagesize();
linebuffer = o_malloc( linebufsize, "unable to prepare linebuffer for read_datafile()\n", __func__, __LINE__ );

fp = fopen( datafilename, "r" );
if ( fp == NULL ) {
fprintf( stderr, "unable to open file %s as data file target\n", datafilename );
exit( 1 );
}


/* read tagname */
{
char *p;
long dataoffset;

if( fgets( linebuffer, linebufsize, fp ) == NULL ) {
fprintf( stderr, "unable to read tagname at file %s\n", datafilename );
exit( 1 );
}
remove_trailing_newline( linebuffer );

p = strstr( linebuffer, "tagname\t" );
if ( p != linebuffer ) {
fprintf( stderr, "unable to find \"tagname\t\" indicator in %s\n", datafilename );
exit( 1 );
}

dataoffset = strlen( "tagname\t" );

newdata->element_name = strdup( linebuffer + dataoffset );
if ( newdata->element_name == NULL ) {
fprintf( stderr, "unable to copy tagname %s in %s\n",
linebuffer + dataoffset, datafilename );
exit( 1 );
}
#if DEBUG
fprintf( stderr, "tagname was %s\n", newdata->element_name );
#endif/*DEBUG*/
}

/* read Correlation */
{
char *p;
long dataoffset;

if( fgets( linebuffer, linebufsize, fp ) == NULL ) {
fprintf( stderr, "unable to read Correlation at file %s\n", datafilename );
exit( 1 );
}
p = strstr( linebuffer, "Correlation\t" );
if ( p != linebuffer ) {
fprintf( stderr, "unable to find \"Correlation\t\" indicator in %s\n", datafilename );
exit( 1 );
}
dataoffset = strlen( "Correlation\t" );
if ( strncasecmp( linebuffer + dataoffset, "OK", 2 ) != 0 ) {
newdata->correlation = 0;
} else {
newdata->correlation = 1;
}
#if DEBUG
fprintf( stderr, "correlation was %d\n", newdata->correlation );
#endif/*DEBUG*/
}


/* read sum */
{
char *p;
long dataoffset;
int res;

if( fgets( linebuffer, linebufsize, fp ) == NULL ) {
fprintf( stderr, "unable to read sum at file %s\n", datafilename );
exit( 1 );
}
p = strstr( linebuffer, "sum\t" );
if ( p != linebuffer ) {
fprintf( stderr, "unable to find \"sum\t\" indicator in %s\n", datafilename );
exit( 1 );
}
dataoffset = strlen( "sum\t" );
newdata->sum = 0.0;
res = sscanf( linebuffer + dataoffset, "%lf", &( newdata->sum ));
if ( res != 1 ) {
perror( "sscanf failed when reading sum.\n" );
exit( 1 );
}
#if DEBUG
fprintf( stderr, "sum was %lf\n", newdata->sum );
#endif/*DEBUG*/
}


/* read mean */
{
char *p;
long dataoffset;
int res;

if( fgets( linebuffer, linebufsize, fp ) == NULL ) {
fprintf( stderr, "unable to read mean at file %s\n", datafilename );
exit( 1 );
}
p = strstr( linebuffer, "mean\t" );
if ( p != linebuffer ) {
fprintf( stderr, "unable to find \"mean\t\" indicator in %s\n", datafilename );
exit( 1 );
}
dataoffset = strlen( "mean\t" );
newdata->mean = 0.0;
res = sscanf( linebuffer + dataoffset, "%lf", &( newdata->mean ));
if ( res != 1 ) {
perror( "sscanf failed when reading mean.\n" );
exit( 1 );
}
#if DEBUG
fprintf( stderr, "mean was %lf\n", newdata->mean );
#endif/*DEBUG*/
}


/* read sqsum */
{
char *p;
long dataoffset;
int res;

if( fgets( linebuffer, linebufsize, fp ) == NULL ) {
fprintf( stderr, "unable to read sqsum at file %s\n", datafilename );
exit( 1 );
}
p = strstr( linebuffer, "sqsum\t" );
if ( p != linebuffer ) {
fprintf( stderr, "unable to find \"sqsum\t\" indicator in %s\n", datafilename );
exit( 1 );
}
dataoffset = strlen( "sqsum\t" );
newdata->sqsum = 0.0;
res = sscanf( linebuffer + dataoffset, "%lf", &( newdata->sqsum ));
if ( res != 1 ) {
perror( "sscanf failed when reading sqsum.\n" );
exit( 1 );
}
#if DEBUG
fprintf( stderr, "sqsum was %lf\n", newdata->sqsum );
#endif/*DEBUG*/
}


/* read subpart */
{
char *p;
long dataoffset;
int res;

if( fgets( linebuffer, linebufsize, fp ) == NULL ) {
fprintf( stderr, "unable to read subpart at file %s\n", datafilename );
exit( 1 );
}
p = strstr( linebuffer, "subpart\t" );
if ( p != linebuffer ) {
fprintf( stderr, "unable to find \"subpart\t\" indicator in %s\n", datafilename );
exit( 1 );
}
dataoffset = strlen( "subpart\t" );
newdata->subpart = 0.0;
res = sscanf( linebuffer + dataoffset, "%lf", &( newdata->subpart ));
if ( res != 1 ) {
perror( "sscanf failed when reading subpart.\n" );
exit( 1 );
}
#if DEBUG
fprintf( stderr, "subpart was %lf\n", newdata->subpart );
#endif/*DEBUG*/
}


/* read datas */
{
char *p;
long dataoffset;
int res;

if( fgets( linebuffer, linebufsize, fp ) == NULL ) {
fprintf( stderr, "unable to read datas at file %s\n", datafilename );
exit( 1 );
}
p = strstr( linebuffer, "datas\t" );
if ( p != linebuffer ) {
fprintf( stderr, "unable to find \"datas\t\" indicator in %s\n", datafilename );
exit( 1 );
}
dataoffset = strlen( "datas\t" );
newdata->numdata = 0;
res = sscanf( linebuffer + dataoffset, "%ld", &( newdata->numdata ));
if ( res != 1 ) {
perror( "sscanf failed when reading datas.\n" );
exit( 1 );
}
#if DEBUG
fprintf( stderr, "datas was %d\n", newdata->numdata );
#endif/*DEBUG*/
}

/* read data list */
{
long i;
int res;

newdata->datalist
= malloc( sizeof( double ) * newdata->numdata );
if ( newdata->datalist == NULL ) {
fprintf( stderr, "not enough memory for datalist: %s\n", datafilename );
exit( 1 );
}

if( fgets( linebuffer, linebufsize, fp ) == NULL ) {
fprintf( stderr, "unable to read empty line at file %s\n", datafilename );
exit( 1 );
}

for ( i = 0; i < newdata->numdata; i++ ) {
if( fgets( linebuffer, linebufsize, fp ) == NULL ) {
fprintf( stderr, "unable to read datas at file %s\n", datafilename );
exit( 1 );
}
res = sscanf( linebuffer, "%lf", &( newdata->datalist[i] ));
/* if we failed in reading data, that means that element should be 0.0 */
if ( res != 1 ) {
newdata->datalist[i] = 0.0;
}

#if DEBUG
fprintf( stderr, "datas was %lf\n", newdata->datalist[i] );
#endif/*DEBUG*/
}
}

fclose( fp );
return newdata;
}

void
add_data_array( data **datalistpp, data *new_datap )
{
if (( *datalistpp ) == NULL ) {
new_datap->next = NULL;
new_datap->prev = NULL;
(*datalistpp) = new_datap;
} else {
new_datap->next = (*datalistpp);
new_datap->prev = NULL;
(*datalistpp)->prev = new_datap;
(*datalistpp) = new_datap;
}
}


void calculate( data *data_list )
{
data *one, *two;
double cor, cor2;
int i;

/* First, change all the data in datalist[] to be subtraction of each's mean */
for ( one = data_list; one != NULL; one = one->next ) {
for ( i = 0; i < one->numdata; i++ ) {
one->datalist[i] -= one->mean;
}
}


for ( one = data_list; one != NULL; one = one->next ) {
for ( two = one->next; two != NULL; two = two->next ) {
cor = 0.0;

for ( i = 0; i < one->numdata; i++ ) {
cor += one->datalist[i] * two->datalist[i];
}
cor /= ( one->subpart * two->subpart );
cor2 = cor * cor;

fprintf( stdout,
"%s\t%s\t%lf\t%lf\n",
one->element_name,
two->element_name,
cor, cor2 );
}
}
}

void remove_trailing_newline( char *linebuffer )
{
size_t len;

while( 1 ) {
len = strlen( linebuffer );
if ( linebuffer[len-1] == '\n' ) {
linebuffer[len-1] = '\0';
} else {
break;
}
}
}


void
help( char *programname )
{
fprintf( stderr,
"%s <filename containing target>\n",
programname );
exit( 127 );
}

void*
o_malloc( size_t size, const char *msg, const char *func, const long line )
{
void *p;
p = malloc( size );
if ( p == NULL ) {
fprintf( stderr, "%s:%ld: %s", func, line, msg );
exit( 1 );
}
}

みての通り、ファイルを読んでくるところの処理が延々。実際の計算部分は calculate() の部分だけ。しかもこれすら標準出力への出力コードが含まれているのに、このサイズ。あぁ、Cの最大の弱点はファイルIOの記述/メモリ管理の記述が面倒くさいことだなぁ。

ちなみに。「C++の例外処理で書けばいいじゃない」という提案をいただいた。が、その手段は使えないし、趣味として使わない。例外処理は素晴らしい機能だ。ただし、あれは「コード的には正しいが、動作環境との兼ね合いで不幸に至った場合」に、どこで不幸を食らったかに依存せず「不幸を食らった事実だけ」を書けばよいから素晴らしいのだ。

しかし、上記のコード、多分端々に見えているだろうが、「おかしくなるのは私のコーディングが間違っているから」というケースに対応するためのものだ。実際に発生した現象としては:
  1. tagname を引っ張ってきたのはいいが、行末の "\n" を削り忘れていた。
  2. Correlation のOK を case insensitive にチェックしているが、実は最初のコードでは "OK" に対して入力は "OK\n" で合致しない攻撃をくらいまくっていた。
  3. Correlation が NG の場合、計算対象からはずすためにリンクリストに登録しないのだが、そのメモリを「放置」していた(メモリリークだ)。64bit環境では特に問題は無かった(ちょっと swap IO するだけ)のだが、32bit環境に持ってきたらパンクした。
  4. Correlation 自身はOKであっても、データが全フィールドに渡って存在するとは限らない。存在しないエントリは 0.0 として扱うことになっていたのだが、sscanf() の戻り値をチェックするときにそのことを完全に失念していて、おかしな部分で give up して exit() していた。
まぁ、そういうケースに対応するための「デバッガコード」であって、異常系ではなかったりする。例外処理だと、「どこでおかしいのか」も判らないし、特に 1 のケースなどは assert() にもひっかけようがない。

先にも書いたが、1データ組み合わせ当たり 6.74usec で計算できた。はーやーいー。9msec とは比べ物にならない。やっぱりコンパイラ言語は本気を出すとすごいわ。

2009年2月5日

リファクタリング-4-

リファクタリング-3-で書いた「データ列を1データ列1ファイルに分割する」プログラムをまず、作った。

remove_NaN.pl:
#! /usr/bin/perl
#
# remove_NaN.pl <dataFileName> <template of eachoutput>
# chop dataFile into each row. If each row is clear that
# "Pearson product-moment correlation coefficient" calculation
# will ends in NaN, do not output that entry.
#
# For <template of eachoutput> I mean something like "hogehoge%08d".
# output file will have row number as for %08d part (%08d can be %d
# or anything in integer format ).

# use Math::NumberCruncher;
# $NCref = Math::NumberCruncher->new();

our $at_once = 7000;

$datafilename = $ARGV[0];
$outtemplate = $ARGV[1];

if ( $outtemplate eq '' ) {
$outtemplate = 'temp%08d.TSV';
}

open( DATAFILE, $datafilename ) or die "unable to open $datafilename as datafile\n";


# First, read first line. It's TAG-NAME list.

our @tagname;
our $line;

$line = <DATAFILE>;
@tagname = split /\t/, $line;
for ( $i = 0; $i < scalar( @tagname ); $i++ ) {
$tagname[$i] =~ s/^\s*(".*")\s*$/\1/;
}


# Okey. Now let's read each rows.
# First row is not numeral, but is time of when data was sampled. so skip first row.

our $filecounter = 0;
for ( $row = 0; $row < scalar( @tagname ); $row += $at_once ) {
debug( "$row start\n" );
# bring file pointer to top of line.
seek( DATAFILE, 0, 0 );

# skip tagname line.
$line = <DATAFILE>;

my @output;
my @data;
my @sum;
my @mean;
my @sqsum;
my @subpart;
my @datas;

for ( my $i = 0; $i < $at_once; $i++ ) {
$output[$i] = 1;
}

while (<DATAFILE>) {
my @oneline = split /\t/, $_;
my $val;

for ( my $i = 0; $i < $at_once; $i++ ) {
$oneline[$row + $i] =~ s/^\s*"(.*)"\s*$/\1/;
$val = $oneline[$row + $i];
if ( $val eq '' ) {
$output[$i] = 0;
} else {
push @{$data[$i]}, $val;
$sum[$i] += $val;
}
}
}
debug( "read data\n" );

# calculate $mean and $subpart.

for ( my $i = 0; $i < $at_once; $i++ ) {
if ( $output[$i] ) {
$datas[$i] = scalar( @{$data[$i]} );
$mean[$i] = $sum[$i] / $datas[$i];
if ( $mean[$i] eq 'NaN' ) {
$output[$i] = 0;
next;
}

for ( my $j = 0; $j < $datas[$i]; $j++ ) {
my $val;
$val = $data[$i][$j] - $mean[$i];
$sqsum[$i] += $val * $val;
}
$subpart[$i] = sqrt( $sqsum[$i] );

if (( $subpart[$i] eq 'NaN' )||( $subpart[$i] == 0.0 )) {
$output[$i] = 0;
}
}
}
debug( "calc data\n" );

# dumpout the row.
for ( my $i = 0; $i < $at_once; $i++ ) {
my $filename = sprintf( $outtemplate, $filecounter );
if ( !defined( $tagname[$row+$i] )){
next;
}

open( OUTPUT, "> $filename" ) or die "unable to open $filename as output file. Template was $outtemplate.\n";
print OUTPUT "tagname\t$tagname[$row+$i]\n";
print OUTPUT "Correlation\t" . ( $output[$i] ? 'OK' : 'NG' ) . "\n";
print OUTPUT "sum\t$sum[$i]\n";
print OUTPUT "mean\t$mean[$i]\n";
print OUTPUT "sqsum\t$sqsum[$i]\n";
print OUTPUT "subpart\t$subpart[$i]\n";
print OUTPUT "datas\t$datas[$i]\n";
print OUTPUT "\n";

foreach ( @{$data[$i]} ) {
print OUTPUT "$_\n";
}

close OUTPUT;
debug( "output $filename \n" );

$filecounter++;
}

ROWSTEP_CLEANUP:
undef @output;
undef @data;
undef @sum;
undef @mean;
undef @subpart;
undef @datas;
}

close DATAFILE;

sub debug {
my $time = gmtime();
print STDERR "$time: ";
foreach ( @_ ) {
print STDERR $_;
}
}

このプログラム中、唯一判らないのは「$at_once」だろう。これはようするに、読んだデータの内、$at_once 個のデータ列づつ平均値だのなんだのを計算して、各データ列ごとに別のファイルに出力する、という事だ。

実はこのプログラム、かなりメモリを消費する。データファイル自体は1行づつ読んではパースし、不要な部分は捨てているのだが、元のデータが 27000列とかだと、一日分のデータだけでも 32bit Windows システムではメモリが足りずに途中でハングする。そこで、一度に処理する行数をスクリプト中に書いておくことにした。メモリが足りないならここを調節すればよい。

ただし、可能な限り大きな値を指定しておくことをお勧めする。実験として50を選んであるが、ものすごく遅くて難儀した。27000の場合、540回同じファイルを読み直すことになり、そのたびにファイル全体を文字列処理する必要があるのだから、当たり前と言えば当たり前だが。

2009年2月4日

リファクタリング-3-

Math::NumberCruncherのコードの一部を手元に展開した状態で読んでいく…ん? んん?? んんん~~~???

どうみても、Correlation() の計算式は
ピアソン積率相関係数 のそれではないっ!!!

確かに似たような値が出てきてはいるが、明らかに違うっ!!!

ぬぅ。なんということだ。大前提からしてやり直しかっ!!!

しかも。これは致命的な問題ではないからいいようなものの…同モジュールに含まれている平方根を出す関数 SqrRoot() は

0.0 を入れると NaN を返すっ!!!
まてぃぃいいいいい!!!

どこの世界に0.0 の平方根が計算できない数値演算ライブラリがあるっ!!! ちなみに、他の値を入れると(プラスの値であれば)妥当な出力を出してくる。どうやら、0.0 の場合におかしくなるような漸近式を使っているらしい。

まぁ、0.0だろうが NaN だろうが、その結果を分母に持ってきて分数を計算すれば、NaNになるのは変わらないので、そりゃ結果だけ見れば構わないと言えば構わないのだが…。とりあえず、この程度のデバッグもできていないモジュールである以上、これは信頼するわけには行かぬ。

というわけで根こそぎ作り変えてくれよう。
!!決定!!

とりあえず、次のようなデザインにする。
  1. まず、データ列を1データ列1ファイルに分割する。
  2. そのデータ列には、当該データ列の平均値、平均値と各データの二乗和、さらに二乗和の平方根、の3つは最低限でも、データとして記録する。
  3. できればデータの個数も記録する。
  4. もちろん、ラベル名も記録する。
  5. いうまでもなく、データ列の値そのものも1行1エレメントで記録する。
  6. 2 で得られた値のどれかが NaN になるか、あるいは「二乗和の平方根」が 0.0 になる場合は、そのようなデータ列は当該データファイルを作らない。
  7. このファイルはすべて文字列で数値を記録する。
で、このファイルを複数読み込んで、組み合わせ毎に計算するプログラムを別途作る。各データ列ごとの事前計算は全部記録されているので、本来の計算に集中できる…に違いない。

…それにしてもここまで巻き戻るとは…

2009年2月1日

リファクタリング-2-

リファクタリングとして数多くのことをする必要があるが、とりあえず最初にやるべきことは、Math::NumberCruncher のコードから、今回必要なルーチンをコピーして持ってくることだろう。Correlation()関数を呼び出す回数は O(n2) になるが、Wikipediaの「相関係数」のページによると相関係数を求める式はこのようになっている:

分母が特に「列」単位で独立して計算できる事がわかる。列単位ならば計算はO(n)で済む。このような最適化がかけられるかどうか、調べるには元のソースが無くてはいけない。
幸い、CPANには perl のソースが .pm ファイルとしてついてくる。それをコピーしてみたが、全部 perl で書かれているようだ。これならば最適かもかけられよう。

次に、x と y のラベル名を読み込むコードを変更する。旧来:
our $i = 0;
our @x_tagname = ();
while (<COMPARE_X>) {
$_ =~ s/^\"//;
$_ =~ s/\"\n$//;
$x_tagname[$i] = $1;
$i++;
}
close COMPARE_X;
となっていたのだが、

  1. push()関数を使えばインデックスの $i は不要になる。

  2. ラベル名に " がついたままでもいいじゃないか。どうせ x, y だけでなくデータのラベルも " がついたままなのだから。文字列比較の際に " がついたまま比較すればいい

と考えるた。結果、このように簡単になる:
our @x_tagname = ();
while (<COMPARE_X>) {
$_ =~ s/^\s*(".*")\s*$/\1/;
push @x_tagname, ( $_ );
}
close COMPARE_X;
同じ変更は COMPARE_Y を読み込む部分についても行う。DATAFILEの最初の1行目を読んだ部分に関してはもっと極端だ。
# Read first line of .
# This should be the list of TAG-NAME.
$line = <DATAFILE>;
@tagname = split /\t/, $line;
for ( $i = 0; $i < scalar( @tagname ); $i++ ) {
my $val;
$val = $tagname[$i];
$val =~ s/^\s*"//;
$val =~ s/"\s*$//;
$tagname[$i] = $val;
}
というこれは
$line  = <DATAFILE>;
@tagname = split /\t/, $line;
for ( my $i = 0; $i < scalar( @tagname ); $i++ ) {
$tagname[$i] =~ s/^\s*(".*")\s*$/\1/g;
}
とできる。

次に。今回のリファクタリングで、BestFit() を呼び出し、回帰直線の傾きと切片を求めるのをやめる。この計算も結構な高コストである上に、相関係数を求めた結果として、重要と思われたものについてのみ必要な数値だからだ。これらは、計算すべきターゲットが見つかってから計算するのでも間に合う。

2009年1月31日

リファクタリング

じゃぁ、相関とかを求めるかね。」で公開した、calcr2.plに新たなる任務が与えられた。

calcr2.pl は「与えられたTSVファイルの2列目のデータと、2列目以降最後までのデータとの相関を求めることができた。これは「ヒント」となるべき兆候があり、それを2列目に置く事ができたから可能だった方法だ。
新しいミッションはこうだ:

データを与えるTSVファイルのフォーマットは従来と変わらない。
このデータとは別に2つのファイル x と y を与える。x,yそれぞれにラベル名が入っているので、x と yの全組み合わせについてピアソン積率相関係数を求めなさい。

今度はノーヒント、というわけだ。

とりあえず、x と y というファイルのフォーマットを決める。と言っても、ラベル名を列挙するだけなのだから、1行1ラベル名にするだけだ。こんな感じ:
"LogicalDisk(C:)\% Disk Read Time"
"LogicalDisk(C:)\% Disk Time"
"LogicalDisk(C:)\% Disk Write Time"
"LogicalDisk(C:)\% Free Space"
"LogicalDisk(C:)\% Idle Time"

ラベル名の前後に " をつけてあるのは、それら全体で一塊、ということを示しているのにすぎない。つーかそもそもこれ自体もとのデータファイルの第1行目を分割して作っているので、実は " をはずすのが面倒だった、という事に過ぎない。

呼び出しのルールはこうしよう:
$ correlation.pl <x> <y> <data>

xのリスト、yのリスト、そして最後にデータだ。データファイルは相変わらずWindows用の perfmon.exe の出力である .blg ファイルを relog.exe で TSV フォーマットに変換した結果、とする。

「ふはははは、そんなもの朝飯前だよ、明智君」

とばかりにちょろっと直してみた結果がこれだ。
#! /usr/bin/perl
#
# correlation.pl <x tagname> <y tagname> <datafile tsv from exe relog>
#

use Math::NumberCruncher;
$ref = Math::NumberCruncher->new();

use open IN => ":utf8", OUT => ":utf8";


open( COMPARE_X, $ARGV[0] ) or die "unable to open $ARGV[0]";
open( COMPARE_Y, $ARGV[1] ) or die "unable to open $ARGV[1]";
open( DATAFILE, $ARGV[2] ) or die "unable to open $ARGV[2]";

# First, read file to get the list we need to calculate correlation with.
our $i = 0;
our @x_tagname = ();
while (<COMPARE_X>) {
$_ =~ s/^\"//;
$_ =~ s/\"\n$//;
$x_tagname[$i] = $_;
$i++;
}
close COMPARE_X;

our $i = 0;
our @y_tagname = ();
while (<COMPARE_Y>) {
$_ =~ s/^\"//;
$_ =~ s/\"\n$//;
$y_tagname[$i] = $_;
$i++;
}
close COMPARE_Y;


# Read first line of <datafile>.
# This should be the list of TAG-NAME.
$line = <DATAFILE>;
@tagname = split /\t/, $line;
for ( $i = 0; $i < scalar( @tagname ); $i++ ) {
my $val;
$val = $tagname[$i];
$val =~ s/^\s*"//;
$val =~ s/"\s*$//;
$tagname[$i] = $val;
}

our $line = 0;
while (<DATAFILE>) {
my @oneline = split /\t/, $_;
for ( $i = 0; $i < scalar( @oneline ); $i++ ) {
my $val;
$val = $oneline[$i];
$val =~ s/^\s*"//;
$val =~ s/"\s*$//;
$data[$i][$line] = $val;
}
$line++;
}
close DATAFILE;

#######
# So we read all the necessary data.
#
# Now calculate colerration.
#
#######

# First, check where each x_tagname is.
XTAG_LOOP: for ( my $x = 0; $x < scalar( @x_tagname ); $x++ ) {
my $localxval = quotemeta $x_tagname[$x];
for ( my $y = 0; $y < scalar( @tagname ); $y++ ) {
if ( $tagname[$y] =~ m/$localxval/ ) {
$x_tags[$x] = $y;
next XTAG_LOOP;
}
}
if (( $x <= scalar( @x_tagname ))&&( !defined( $x_tags[$x] ) )) {
die "Unable to find $x th tag \"$x_tagname[$x]\" as tag name for giving list.";
}
}

# Second, check where each y_tagname is.
YTAG_LOOP: for ( my $y = 0; $y <= scalar( @y_tagname ); $y++ ) {
my $localyval = $y_tagname[$y];
for ( my $x = 0; $x < scalar( @tagname ); $x++ ) {
if ( $tagname[$x] =~ m/$localxval/ ) {
$y_tags[$y] = $x;
next YTAG_LOOP;
}
}
if (( $y < scalar( @y_tagname ))&&( !defined( $y_tags[$y] ) )) {
die "Unable to find $y th tag \"$y_tagname[$y]\" as tag name for giving list.";
}
}

# print the tag line.
print "\"\"\t";
for ( my $x = 0; $x < scalar( @x_tagname ); $x++ ) {
print "\"$x_tagname[$x]\"\t\"\"\t\"\"\t\"\"";
if ( $x < scalar( @x_tagname ) - 1 ) {
print "\t";
} else {
print "\n";
}
}

# print the 2nd tag line.
print "\"\"\t";
for ( my $x = 0; $x < scalar( @x_tagname ); $x++ ) {
print "\"r\"\t\"a\"\t\"b\"\t\"r2\"";
if ( $x < scalar( @x_tagname ) - 1 ) {
print "\t";
} else {
print "\n";
}
}

for ( my $y = 0; $y < scalar( @y_tags ); $y++ ) {
print "$y_tagname[$y]\t";
for ( my $x = 0; $x < scalar( @x_tags ); $x++ ) {
my $r = Math::NumberCruncher::Correlation( \@{$data[$y_tags[$y]]}, \@{$data[$x_tags[$x]]} );
my ($a, $b) = Math::NumberCruncher::BestFit( \@{$data[$y_tags[$y]]}, \@{$data[$x_tags[$x]]} );
my $r2 = $r * $r;

print "\"$r\"\t\"$a\"\t\"$b\"\t\"$r2\"";
if ( $x < scalar( @x_tags ) - 1 ) {
print "\t";
} else {
print "\n";
}
}
}


実行してみる………… zzzzzzz …………

おーそーいーーーーーー!!!
救いがたいほどに遅い。あわててデータを見てみる。

ぶっっ!!!


調べる相手だけで、12791項目もある…今までが3500とかのレベルだったから4倍近い。何も考えずに相関を総当りで取ったら、今までの16倍時間がかかる。只でさえ遅いのに…。

こうしてスピードアップを余儀なくされたのだが、そもそもそういう小細工をするには、ちょっとコードが汚すぎるし、外部ライブラリに依存しすぎている。というわけで、リファクタリングを開始することにした。

この物語は、そのリファクタリングの記録である…というわけで続く。