以下は、perfbook の15章の kanda.motohiro@gmail.com による全訳です。編集に不便なので、ファイルを分けました。perfbook の訳の他の部分は、親文書を参照。
15章 使いやすさ
「完璧なAPIを作ることは、完全犯罪を犯すのに似ています。少なくても50の失敗する可能性があり、もしあなたが天才ならそのうち25を事前に知ることができます。」
15.1 簡単とはどういうこと?
簡単とは相対的な言葉です。例えば、多くの人は15時間、飛行機に乗り続けることを少し、苦痛と感じます。ただ、その他の移動手段、特に泳ぐ、の事を考えるとそうでもありません。これはつまり、使いやすいAPIを作るには、あなたが、あなたの対象とする利用者についてよく知っている必要があるということです。
以下の質問はこの点を明らかにします。「今、生きている人全部の中から、ランダムに一人を選んだら、どんな一つの変更が彼あるいは彼女の人生を改善するでしょう?」
全ての人の人生を助けると保証される単一の変更はありません。結局、人々の範囲はとんでもなく広く、その必要、要望、欲求、そして熱望にもそれと対応する広い範囲があります。飢える人は食料が必要でしょう。しかし、余分の食料は病的肥満な人の死期を早めることも十分にあるでしょう。多くの若い人が激しく求める高レベルの興奮は、心臓発作から回復しつつある人にとっては致命的かもしれません。ある人の成功に致命的な情報は、情報過多に悩む他の人には、失敗の元となるかもしれません。短く言えば、もしあなたが、あなたが一切知らない人を助けることを意図するソフトウェアプロジェクトの仕事をしているならば、その人があなたの努力に対して、あまり感銘を受けないことに驚いてはいけません。
あなたが本当にある人々のグループを助けたいならば、その人たちとかなりな時間、一緒に緊密に作業をする事に代わるものは単純に何もありません。とは言え、あなたの利用者があなたのソフトウェアによって幸せになる確率を増やすために、あなたにできる単純なことがいくつかあります。次の節では、そのうちいくつかを述べます。
15.2 API 設計のための Rusty の錆びたものさし
この節は、Rusty Russell の2003年オタワ Linux シンポジウムのキーノート発表の部分からとったものです。Rusty の鍵となる主張は、ゴールはただAPIを使いやすくするのではなくて、誤って使うのを難しくするべきだということです。最後に Rusty は、自分の錆びたものさしを提示して、この重要な、誤って使うのが難しい性質を、悪くなる順に示しました。
以下のリストは、 Linux カーネルに限らず一般化した、錆びたものさしです。
1. 間違うのは不可能です。これはすべてのAPI設計者が目指すべき標準ですが、神秘的な dwim() コマンドだけがそれに近いところまで行くことができるだけです。
脚注
dwim() 関数は、「do what I mean 、私の意味するとおりにしなさい」の略です。
2. コンパイラあるいはリンカが間違ったことをさせません。
3. コンパイラあるいはリンカが、あなたが間違うと警告します。
4. 最も単純な使い方が正しいものです。
5. 名前を見れば使い方がわかります。
6. 正しく動くか、そうでない時には必ず実行時に失敗します。
7. 一般的な慣習に従えばうまくいきます。malloc() ライブラリ関数が良い例です。メモリー確保を間違うのは簡単ですが、とても多くのプロジェクトがそれを、少なくてもほとんどの場合、うまくやることができます。malloc() を、Valgrind と一緒に使えば、malloc() は、ものさしの「正しく動くか、そうでない時には必ず実行時に失敗」の目盛りにまで引き上げることができます。
8. 文書を読めばうまくいきます。
9. 実装を読めばうまくいきます。
10. 正しいメーリングリストアーカイブを読めばうまくいきます。
11. 正しいメーリングリストを読んでも失敗します。
12. 実装を読んでも失敗します。最初の、rcu_read_lock() の、CONFIG_PREEMPT でない実装はこのものさしの目盛りの悪名高い例です。
13. 文書を読んでも失敗します。例えば、DEC Alpha wmb 命令の文書は、多くの開発者をこの命令が実際よりもずっと強いメモリオーダーセマンティックを持つものであると勘違いさせました。後の文書は、この点を明確にし、その結果、wmb 命令を、「文書を読めばうまくいきます」の目盛りにまで引き上げました。
14. 一般的な慣習に従っても失敗します。printf() 文はこのものさしの目盛りの例です。なぜならば、開発者はほとんど常に printf() のエラー戻り値をチェックしません。
15. 正しくやっても、実行時に失敗します。
16. 名前は、それを使ってはいけないやり方を示します。
17. 自明な使い方は誤りです。 linux カーネルの smp_mb() はこのものさしの目盛りの例です。多くの開発者は、この関数は実際よりもずっと強いオーダリングセマンティックを持つと推定します。14.2節と、linux カーネルソースツリーの Documentation ディレクトリに、この誤りを避けるために必要な情報があります。
18. コンパイラあるいはリンカは、あなたが正しいことをすると警告します。
19. コンパイラあるいはリンカは、あなたが正しいことをするのを許しません。
20. 正しくすることは不可能です。gets() 関数は、このものさしの目盛りの有名な例です。実際、gets() は、無条件のバッファオーバーフローセキュリティホールというのが最もふさわしいでしょう。
15.3 マンデルブロ集合のひげをそる
役に立つプログラムのセットは、マンデルブロ集合(図15.1を参照)に似ています。それは、クリアカットでスムーズな境界を持たないという点においてです。もしそんなことがあれば、halting problem は解決可能です。でも、私達には、実際の人々が使うことのできる API が必要です。それぞれの全ての潜在的使用に対して、博士号を修了する必要があるものではいけません。なので、私達は、「マンデルブロ集合のひげをそります」。API の使用を、潜在的な用途の完全セットから、簡単に説明できるサブセットに制限します。
脚注
Josh Triplett の言葉
そのようなひげそりは、生産性を下げるように思えます。結局、あるアルゴリズムが動くなら、なぜそれを使ってはいけないのでしょう?
ある程度のひげそりが、なぜ絶対に必要かを考えるために、デッドロックを防ぐためのあるロック設計を考えましょう。でも、多分、可能な限り最悪な方法で。この設計は、循環二重連結リストを使います。リストは、システムのそれぞれのスレッドごとに、一つの要素と、ヘッダ要素を持ちます。新しいスレッドが生成されると、親スレッドはこのリストに新しい要素を挿入しなくてはいけません。それには何らかの同期が必要です。
リストを守る一つの方法は、グローバルロックを使うことです。しかし、これは、スレッドが頻繁に作成、削除されると、ボトルネックになります。
脚注
オペレーティングシステムの強力なバックグラウンドをお持ちの方は、不信を棚上げ下さい。不信を棚上げできない方は、もっとましな例を送って下さい。
他のアプローチは、ハッシュテーブルを使って、個々のハッシュバケツをロックすることです。しかしこれは、リストを順にたどる時には、性能が悪いでしょう。
3つ目のアプローチは、個々のリスト要素をロックすることです。そして、挿入の時には、前と後の両方の要素のロックを必要とします。両方のロックを取らないといけないので、それらを取る順序を決めないといけません。二つの一般的アプローチは、ロックをアドレス順に取ることと、それらがリストに現れる順に取ることです。その場合、ロックされる二つの要素にヘッダが含まれれば、それが必ず最初に取られるようにします。でも、このどちらの方法も、特別なチェックと分岐が必要です。
ひげそりが必要な解決策というのは、無条件に、ロックをリスト順に取るというものです。でも、デッドロックはどうしましょう?
デッドロックは起きません。
これを考えるために、リストの要素を番号付けします。ヘッダはゼロで、リストの最後の要素(ヘッダの一つ前のものです。リストは循環ですから)はN です。同様に、スレッドをゼロから N-1 に名づけます。もしそれぞれのスレッドが要素の連続するどれか二つをロックしようとしたなら、少なくても一つのスレッドは、両方のロックを取れることが保証されます。
なぜ?
リスト全体をおさえるだけの十分なスレッドが無いからです。スレッド0が、要素0のロックを取ったとします。ブロックされるためには、どれか他のスレッドが既に要素1のロックを取っていなければいけません。なので、スレッド1がそうしたとします。同様に、スレッド1がブロックされるためには、どれか他のスレッドが既に要素2のロックを取っていなければいけません。同様に、スレッド N-1 が、要素 N-1 のロックを取るところまで続きます。スレッド N-1 がブロックされるためには、どれか他のスレッドが既に要素Nのロックを取っていなければいけません。しかし、それ以上スレッドはいないので、スレッド N-1 はブロックされることはありません。なので、デッドロックは起きません。
では、なぜ、私達はこの喜ばしい小さなアルゴリズムの使用を禁止するべきでしょう?
あなたが本当にそれを使いたいなら、私達はあなたを止めることはできないというのは事実です。しかし私達は、私達がかかわる全てのプロジェクトに、そのようなコードが含まれることに反対できます。
でも、あなたがこのアルゴリズムを使う前に、以下のクイッククイズをよく考えて下さい。
クイッククイズ15.1
要素を削除する時に、類似のアルゴリズムを使うことができますか?
事実は、このアルゴリズムは極端に特殊で(限られた長さのリストでしか動きません)、また、とてもこわれやすいということです。誤ってリストにノードを追加することができなかったバグは全て、デッドロックになります。実際、単純にノードを追加するのが少し遅れただけでも、デッドロックになります。
さらに、前述の他のアルゴリズムは、「良くて十分」です。例えば、単純にアドレス順にロックを取るのは、とても単純で高速です。同時に、任意の長さのリストの使用も可能です。なお、空のリストと、一つだけの要素を持つリストという特別ケースに注意!
クイッククイズ15.2
ほほう!このアルゴリズムのようにひげそりが必要なものを考えついた変な奴ってどんな奴なんでしょう???
まとめると、アルゴリズムは、単純に、たまたま動くからという理由で使ってはいけません。その代わりに、それを学ぶ価値があるくらいに役に立つアルゴリズムだけに制限するべきです。アルゴリズムがより難しく、複雑になるならば、それを学び、そのバグをなおす苦痛に価値があるためにも、アルゴリズムはより一般的に役に立たなくてはいけません。
クイッククイズ15.3
この規則の例外をあげなさい。
例外は置いといて、私達はソフトウェア「マンデルブロ集合」のひげそりを続けなくてはいけません。図15.2に示すように、私達のプログラムがメンテナンス可能であり続けるために。
以上