2014年8月15日
まずコードの可読性を最適化しよう
(2014-7-8)by Valentin Simonov
本記事は、原著者の許諾のもとに翻訳・掲載しております。
最近では 最適化 という言葉を使う場合、GPUメモリ消費やネットワークトラフィックの最適化、などと明示的に言わない限りは、 実行時間の最適化 という意味で使われるケースがほとんどです。
自分が何を最適化しようとしているかを知ろう
私がプログラムを始めた頃、プロセッサの処理能力は遅く、メモリサイズもとても限られていて、キロバイト単位で計算されていました。ですからメモリ容量をよく考え、メモリ消費を上手に最適化しなくてはなりませんでした。大学では最適化について2つの極論を教わりました。
- メモリを犠牲にして実行スピードを最適化する。
- または何度も計算を繰り返して、メモリ消費を最適化する。
最近では誰もメモリについては大して気にしていません(デモシーン製作者、組み込みシステムのエンジニア、一部の携帯電話ゲームのディベロッパなどは別です)。RAMだけでなく、ハードディスクの容量についても同様です。 Watch Dogをインストールするとなんと25GBもディスク容量を使用します。ついでに言うと私は今この記事をChromeのタブを使って書いているのですが、130MBのRAMを使用しています。
最適化にはもっといろいろな種類があります。
- 電力消費の最適化。スマートフォンの市場が広がるにつれて注目を浴びるようになりました。
- 可読性の最適化。コードを読んだりデバッグをしたりしやすくして、開発期間とコストを削減する。
- ここで一旦ストップします…
可読性の最適化とは、コードを読みやすく、追いやすく、内容を把握しやすいようにすること。
一度に全てを最適化できないということを理解しておかなければなりません。例えば、パフォーマンスの最適化に取り組んでいる時、アプリケーションはより多くメモリを消費し、コードは読みづらいものになる傾向があります。
なぜ可読性が大事か
ディベロッパはその作業時間のうち、より多くの時間を、コードを書くことではなく読むことに割いている。デバッグをしたり、他の人が行ったコミットをチェックしたり、新しいライブラリを学んだり、することなどに。
コードを読みながら、ディベロッパは本質的には(大して有能でもない)通訳のような仕事をしています。つまり、現在の実行状況を踏まえつつ、脳内でコードを実行させています。だからプログラマというのは作業中に邪魔をされると特に不機嫌になるんです。
時は金なり
よく理解しておく必要がある一番大切なことは、あなたと、あなたの同僚の作業時間にはお金がかかっているということです。どんなにディベロッパが必死で働いたとしても、こんな風に時間を浪費してしまうことが考えられます。
- 現在必要がなく、将来も使われないかもしれないものについての作業。
- 気付くような価値を付加しない作業。
例えば1週間かけて実行時間を最適化した機能が、実際には1時間に一度、10ミリセックずつ呼ばれるだけのものだった場合など。 - デバッグ困難なコードを記述したり、その中からバグを探そうとしたりすること。
- 他の人の作業を手間取らせるようなコードを書くこと。
来週はあなたが“他の人”になるかもしれないということを忘れずに。
ディベロッパが経験を積んでいて、効率的なアルゴリズムやすっきりしたコードを書けるということを想定してもこういうことが考えられます。そうでなければこのリストはとても長くなってしまうでしょう。
可読性の最適化
Donald Knuthによる有名な言葉があります。あなたも幾度となく聞いたことがあるかもしれません。
「時期尚早な最適化は、プログラム開発における全ての(またはほとんどの)諸悪の根源だ」 1974年 D.Knuth
この言葉を暗記していながら、本来の意味を理解していない人は多くいます。最も多い間違いは次のような感じです。
- こんな単純なタスクのコードがどうしてこんなに複雑なの?
- XとYを最適化したんだよ、将来を見越せば…
– 時期尚早な最適化が全ての諸悪の根源だ って聞いたことないのか? - あるとも、だけどこれは 時期尚早な最適化 じゃない。 こうやったら速くなるんだから。
思うに、 「時期尚早な最適化」 という言葉がきちんと定義されていないからこんなことになるのではないでしょうか。だから上述の例で出てくる彼は 「時期尚早な最適化」 をしているなどとは思いもよらないのです。この言葉をどう定義しましょうか。
時期尚早な最適化とは、実環境のシステム上で検証しテストを実行する前に行われるあらゆる最適化について言う。
つまり可読性以外の全てですね。ですから、 やるべきじゃない ことではなく、 やるべき ことについて話しましょう。上の引用はこんな風に書き換えられます。
まず可読性を最適化しよう。
コードを読む時、ディベロッパを手間取らせることとは何か
さて、コードを読みやすくして、それにかける時間を少なくし、お金を無駄にしない、ということに異論はないですよね。でも具体的にこれは何を意味するのでしょうか。
ディベロッパがコードを読んでいる時、作業を大幅に遅らせる2つの根本的な要因があります。
- コードが理解しづらい
- コードが追いかけづらい
コードが理解しづらい
ソフトウェアの解釈プログラム(インタプリタ)であれば、2つの数値を加算して関数を呼び出す(もちろん、その間にコンパイルする)ためのコードを解析するのは、造作もないことでしょうが、残念ながら私たちは人間です。
思った通りに動かない場合、プログラマはソースファイルに入力されたコードがどういう目的でどんな動きをするかを確実に把握しなければなりません。
コードの理解を難しくさせるもの
ここからは、記述されたコードの言語やアプリケーション・ドメインに使われているアルゴリズムに詳しい( すなわち、このコードを十分に理解している )ベテランのディベロッパを想定してお話しします。
- コードがひどい。1文字変数と1000行にわたる関数の山。
- 正確な、または一貫したフォーマットがされていない。
- 不要なワンライナーがある。
- コメントなしに低レベルの最適化が施されている。
- コードが賢すぎる。
最初の2つに関してはスキップします。コードの出来が悪ければ、いずれにしても読む気はしませんからね。社内の誰かがそれを書いたとしたら、正しい書き方を教えてやるか、あるいはクビにでもした方が賢明です。それから、これはもちろんのことですが、コード全体にわたり、しっかりとしたコーディングスタイルを維持するようにしましょう。
3. 不要なワンライナーがある
あるいは行数の最適化とでも言いましょうか。長い1行に入れ子された関数呼び出しや条件演算子(?:)などは、非常にパースしにくいですよね。もちろん、これをある程度、主観的な問題だという人もいると思います。ただ、可読性を犠牲にしても、ソースコードをできるだけ少ない行数にしなければならないと感じている人がいるのも事実です。
4. コメントなしに低レベルの最適化が施されている
以前は、コードの可読性も高く、動作も順調だったとしましょう。でも、ある時点で、誰かがコードの最適化を決行したとします。手間をかけたプロファイリングで、その直後は結果も上々でしたが、最近では、ただの配列、ビット演算子、摩訶不思議な数字の集まりにしか見えなくなりました。なぜコードがそう見えるようになったのか、誰も知りませんし、どうすればそれを解決できるのかも分かりません。なぜなら、最適化の担当者が何1つコメントを残していなかったからです。
優れたコードにはコメントは必要ない、ということを聞いたことがある人もいるかと思いますが、コートが最適化された時には( 例えそれが優れていても 優れている場合はなおさら)、コメントは必要です。
こうした最適化でも、コードベースの このようなコメントなしの行 は、さほど差し障りはありません。
if (val != val) { ... }
5. コードが賢すぎる
私たちはソフトウェアのディベロッパとして、より アカデミックで理論的な 技を、日々学んでいます。そうなると、その技を現場で試してみたくなるものです。詰まるところ私たちは、ただのプログラマではなく、 コンピュータ科学者 なのですからね。
一部の言語は、ディベロッパに最先端の技術の使用を要求するため、コードはより複雑に、より アカデミック(理論優先的) になります。コードを使って、堅固で完璧なシステムを作り上げる達成感は、学識者の99.997%が理解できないような方法を使って、数学の難解な定理を証明してみせるのと同じようなものでしょう。
ただし、例えそのコードがモジュール/クラス/関数にうまくまとめられており、それぞれのブロックが完全に可読性の高い命令型のコードで構成されていたとしても、他の人がこれを読むのは一苦労でしょう。コード全体の構造を把握していなければなりませんし、使われているテクニックやパターンについても知っている必要があるからです。
ここでもう一度。来週はあなたが“他の人”になるかもしれないということを忘れずに。
私の知人でScalaを使っている人が2人しかいないというのは、このことと関係があると思います。個人的には、Scalaは大好きです。 私にとっては、真空中でガラスの城だって作ることができる、学究的な遊び場のようなものだからです。 でも、Scalaを知れば知るほど、その機能を使いこなせばこなすほど、それが 本質的には書き込むだけの言語 (うのみにはしないように!)であることが理解できるようになりました。ただ、Perlほどには書き込み専用ではなく、どれだけ美しいコードベースでも変更や更新は必要になります。
Perl ほどには書き込み専用ではなく、どれだけ美しいコードベースでも変更や更新は必要になります。そしてここで、誰がこの 美しいコード を理解できるかという問題に直面するわけです。
クリーンで賢いコードほど読みづらいというのは、にわかに信じられませんね。
「デバッギングには、最初にコードを書く作業の2倍の労力が必要になる。従って、もしあなたが持てる技術を駆使してそのコードを書いたとしたなら、あなたはつまり、デバッグについてのスマートさに欠けていると言わなければならないだろう。」 Brian Kernighan
コードが追いかけづらい
コードを読む時、ディベロッパはあるメソッドやクラスから別のメソッドやクラスに、頻繁に目を移す(ジャンプする)ものです。この時、お手持ちのIDEを使いこなせると、時間を節約することができます。IDE(例えばVisual Studio)の Go to Declaration 、 Find Usages 、 Navigate to 、 Inspect 、あるいはその他の機能を使うことによって、コードを 連結グラフ として考えることができるようになるのです。
コードを書く時はNotepadで構わないものの、効果的にコードを読みたい場合には、IDEの習得が必須。
ところで、 連結グラフ とは具体的な何なのでしょうか?
位相空間のようなスペース内で連結されているグラフ、すなわちグラフのあるポイントが他のグラフのポイントとパスで連結されている状態( ソース )。
言い換えると、 「連結」 コードでは、コードの役割モデルを頭に描きながら、あるメソッドから別のメソッドを追うことが簡単にできるということです。
もしコードのどこかで連結が切れていれば(IDEがメソッド間の移動をうまくできない時は)、その連結部分は、自分でじっくり見る必要が出てきます。つまり、コード内の非連結部が多い→より追いかけづらい→より読みづらい、という図式が出来上がります。
では、なぜコードのグラフは切断されるのでしょうか?それには多くの理由が考えられますが、よく見るものをここに挙げてみたいと思います。
1. メソッドやプロパティが文字列として参照されている
一部のフレームワークはこれをしがちです。 「コールバック」 を文字列として渡し、必要に応じてリフレクションを使います。この場合、古き良きCMD+Fを使いましょう。
最悪なフレームワークは、動的言語で、これらの文字列を動的にします。 JavaScriptやActionScript 3.0がそうです。
2. コードが別々のパーツに分割されている
例えばコードの半分がC#で記述されており、残りの半分がビジュアルなノードエディタに組み込まれている場合、双方の行き来は大変かと思われます。
これは、 依存性注入フレームワーク やXML構成のものも同様です。あまり言われることはありませんが、XML構成を記述することもコーディングで、宣言型プログラミングと呼ばれます( ちなみにXML上に命令型言語を構築するようなバカげた所業のことではありませんよ )。
3. 巨大なグラフノード
20のリンクから1000行に連なるメソッドにジャンプ? これは痛いですね… このようなノードを含んだグラフに使い道はありません。
4. 全てが抽象化されすぎている
Go to Declaration により、インターフェイスや抽象クラスにジャンプすると、どの実装かを把握しなければなりません。依存性注入、 Abstract Factory 、その他、依存性に抵抗するメソッドなどの場合、状況はさらにひどくなります。コード内のノード連結が抽象的になりすぎるのです。
DI と XML を私が嫌っているような印象を与えているかもしれませんね。DIは スパゲティコード を回避するのに優れたツールで、基本設計をよりモジュラー化し検証可能なものにすることができます。ただ、他の多くの優れたツール同様、誤って使うと大変な目に遭うでしょう。
アプリケーションを検証しようとして、どこから(どこがエントリポイントなのか、など…)手を付けていいのか見当も付かなかった時は、完全に落胆しました。それは全て、巨大なXML構成により魔法のごとく自動的に組み上げられたものだったのです。
XML構成 については、私は確かに嫌いです。
可読性のまとめとして、以下に覚えておくべき点を挙げました。
- IDEをマスターする
- コードグラフをできるだけ連結させる
- 何より、シンプルなコードを記述する
- 不要なコードを書くことは、お金の無駄を意味する
シンプルなコードを書くように努めることは時間がかかりますし、時期尚早な最適化に抵抗することは間違いなく難しいと思います。
でも、納期の2時間前、例えば48時間寝ていないような状況下で、コードがデバッグ可能な状態であったなら、あなたは半分眠った脳みそで、きっと過去のあなた自身に感謝することになるでしょう。
追伸
reddit や hackernews で、ぜひディスカッションに参加してください。
多くのグラマーエラーを修正してくれた /u/Arandur に感謝します。
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa