【前へ】

3.4 プログラミング/コンパイラ関連

3.4.1 変貌するプログラム言語の位置づけ

近山 隆委員

 高性能計算・通信の実現には、ハードウェアとソフトウェアの両面が重要であることは言うまでもない。ソフトウェアの記述には通常なんらかのプログラム言語を用いるわけだが、解くべき問題とハードウェアアーキテクチャの両者の複雑化や、コンパイル技術の進展に伴い、プログラム言語の表面上の記述と、その記述内容の計算機による実現との間の乖離が拡大してきている。このため、従来常識とされてきた、手続き型言語は計算機による計算手順を記述し、宣言型言語ではプログラムを実行する計算機と独立に問題仕様を記述する、といった言語族についての認識は適切性を失いつつある。
 本稿では近年のプログラム言語技術と計算機アーキテクチャ技術により、プログラム言語の位置づけがどのように変化してきたか、今後さらにどのような変化が予測されるかについて述べる。

 1. 手続き型言語と宣言型言語
 計算機のソフトウェアはなんらかのプログラム言語で記述するのが通例である。高性能計算通信システムにも、なんらかのプログラム言語によるソフトウェア記述が欠かせない。プログラム言語は、その設計原理、利用目的とする対象分野、利用者とのインタフェース方法などによって、実に数多くのものが使われてきている。こうしたプログラム言語を分類する軸として代表的なもののひとつが、手続き型か宣言型かという視点からの、ふたつの言語族への分類である。
 手続き型(procedural)あるいは命令型(imperative)と称される言語族は、機械語を直接用いた記述から自然に発展してきた体系といえる。
 計算機の動作を直接的に指示する機械語命令列によるプログラム記述は、たとえば分岐命令の飛び先の命令番地なども、その番地を直接数値として記述するため、プログラムのごく小さな変更にあたっても、かなり大幅なプログラムの修正が必要であった。また、機械語命令のオペレーションコードも直接数値で指定するため、プログラムの読解性がごく低いという問題があった。そこで、こうした指定に文字列による「名前」を用いるようにしたアセンブリ語が使われるようになった。
 ついては、ほとんど同じような機能を持つが、詳細においては異なる部分のある異機種計算機に対し、個別にプログラムを作成する労力が問題となった。ある抽象レベル、たとえばメモリ内容を読み出す、加算を行う、といったレベルでは同じ操作を行うにもかかわらず、機種が異なれば異なる機械命令体系を持っているため、アセンブリ語による記述では異なるプログラムとならざるを得ない。そこで、通常の計算機システムならば必ず持っているような処理機能を抽象化し、そうした機能の系列を記述できるような言語を設計した結果が、Fortranなどの手続き型プログラム言語である。



 このような生い立ちのものであるから手続き型言語は、記憶装置の内容を読み出して CPU を用いた処理を行い、その結果をまた記憶装置に蓄えるという、実際の計算機が共通して持つ機能を、ある程度高い抽象レベルで記述することが出来るような言語になっている。こうした意味で手続き型言語の記述は、どのように計算を進めるか(how)の記述であるということが出来る。
 一方これに対し、まったく異なるアプローチから設計されたのが、関数型や論理型などの宣言型(declarative)、記述型(descriptive)、あるいは非手続き型(non-procedural)と呼ばれる言語族である。宣言型言語によるプログラムの記述は、実際の計算機による計算の過程そのものをいったん離れ、数学や論理学といった形式的体系に基づいて、計算すべきものは何であるか(what)を記述する。記述するのは何を計算するかであって、どうやって計算するかではなく、計算の具体的手順は言語処理系に委ねられる。このことが両言語族にさまざまな違いが生じる根源となっている。

2. 言語族の特徴
 プログラム言語を手続き型と宣言型の両言語族に分類するとき、同じ言語族に属するプログラム言語には、共通した長所・短所がみられる。

2.1 伝統的な見方
 宣言型言語は形式的体系に拠った意味論をもっており、具体的な計算機の機械語から出発して抽象化していった手続き型言語とは異なる性格を持つ。このため、宣言型言語は手続き型言語に比して意味論の抽象度が高く、より高い抽象レベルでの記述が可能で、このため同じ内容のプログラムをより簡潔に記述できるとされてきた。
 たとえば、論理式で表された一定の制約を満たす解を求めるような問題を解く場合、Prologのような論理型言語を用いれば、ほとんど制約論理式の形式を計算機入力に便利な形式に変換するだけの操作でプログラムを構成できることが多い。同じ問題に対するプログラムをCなどの手続き型言語で記述するには、制約論理式を評価手続きとして記述しなければならないのはもちろんのことであるが、その式を満たすような解候補をどのようにして生成するか、どのような順で制約充足の成否を判定するかなど、解法を手順の詳細にわたるまで指定する必要がある。
 一方、手続き型言語プログラムを機械語に翻訳するのは、抽象化の際に通ってきたパスを逆にたどるだけなので、コンパイラなどの言語処理系を構成することは容易である。宣言型言語は計算機と直接的な関係のない形式体系に基づいた意味論を持つため、処理系の構成は容易ではないとされる。効率的な処理の実現のための手がかりは乏しく、そのまま素直に機械語に翻訳すればよい手続き型言語とは大きな違いがある。
 たとえば論理型言語Prologのある程度効率的な処理方式が確立するまでには、言語が提案されてから四半世紀近くもの年月を要しており、この間に同じ計算機ハードウェア上でのProlog処理系の速度性能は二桁以上も向上している。もちろんFortranなどの手続き型言語においても処理系の技術進歩による速度性能向上は小さくないが、せいぜい数倍程度にすぎず、当初から最適に近い処理系が構成可能であったことがわかる。

2.2 宣言型言語による記述は容易か
 前節に述べた手続き型・論理型の両言語族についての長所短所は、一般に広く認められてきたものである。しかしながら、記述の容易性に関していわれてきた長短については、これを精査すると、必ずしも承認しがたい点も明らかになる。
 第一に、宣言型言語による記述が手続き型言語による記述よりも高いレベルであるとは限らない。たとえば、実際に利用されている関数型や論理型の言語は、整数値データを扱うためのプリミティブを備えているが、これは関数型・論理型言語の本質的な機能とはいいがたく、現実の計算機がもつ四則演算機能を直接的に利用するための付加的機能という面が強い。実際、多くの Prolog 処理系では、整数値データに対する四則演算は、通常のPrologの述語が備えている入出力関係に関する双方向性を持たず、いわばままこ扱いである。
 宣言型言語においては、むしろ自然数をペアノの公理に基づいて、宣言的な基本機能だけを用いて記述する方が、自然な記述であるといえよう。こう考えると、数値演算を必要とする応用について、少なくとも数値演算の部分についてのみ考えれば、機械語の四則演算命令をほぼそのまま利用することを想定して設計されている手続き型言語の方が高い抽象度を持つといえる。
 同様なことは他の多くの機能についても言える。宣言型言語は採用した形式体系に沿った機能を持っているため、どのような問題に対してもその形式体系の抽象レベルでの記述が可能になる。一方、手続き型言語は計算機の物理的なハードウェアに近い機能については、むしろ宣言型言語よりも抽象度の高い記述が可能である。前述の論理型言語による制約充足問題の記述などは、計算機のハードウェア機能との直接的なマッチングが難しく、形式論理という論理型言語の依拠する形式体系とのマッチングがよかったため、論理型言語による記述が容易であったのだ、ともいえよう。逆に、たとえば時系列が重要な要素となるグラフィック・ユーザ・インタフェースを、手順を記述の基本要素としない宣言的な枠組で記述することは、不可能ではないにせよ独自の難しさがある。抽象度の高低は単純に一次元に並べられる全順序関係ではなく、問題領域によって異なる複雑な基準なのである。
 また、近代的なプログラム言語ならば何を用いても、何らかの形でプログラムライブラリを提供することができる。ライブラリを適切に構築すれば、問題領域に対する抽象度の高いプリミティブを提供できる。どのようなライブラリが利用可能であるかは、その言語のもつ歴史的社会的背景に依存するところが大きいが、少なく言語自体の問題としても、プログラムライブラリを適切な形で提供しやすいか否かは重要な特性であり、これは言語プリミティブの抽象度とは簡単な相関にはない。
 第二に、仮に抽象度の高い簡潔な記述が可能であっても、それがプログラマにとって容易であるとは限らない。抽象概念は抽象概念を扱い慣れている者にとっては容易に扱えても、抽象概念を苦手とするものにとっては、むしろその簡潔性ゆえに理解も利用も難しくなることがある。筆者自身の経験からしても、用いる体系に適切な抽象度がどのレベルであるかには大きな個人差があり、簡潔な抽象レベルの記述よりも、煩瑣な具体レベルの記述の方が、はるかに理解しやすい、といった例は少なくない。
 以上のとおり、記述の抽象レベルの高低や難易と、手続き型・宣言型の言語族の優劣との関係は、従来いわれてきたような単純な関係にはなさそうで、必ずしも宣言型が有利とは限らない。要は解くべき問題と言語の依拠するモデルとのマッチングの問題であるといえよう。

2.3 最適化
 手続き型言語と宣言型言語の間での記述性の優劣については、前節に述べたとおり問題とのマッチングの問題であると考えられるが、性能上の優劣については、明らかに手続き型言語の優位性が認められるように見える。実際、大規模数値計算はいうに及ばず、宣言型言語が記述の簡潔さの面では明らかに優位にある組合せ的な問題においても、大規模な問題を解くための実用システムは手続き型言語で記述するのが通例である。
 性能の問題を議論するためには、プログラムに対するいわゆる「最適化」の問題を避けて通れない。プログラムの性能を比較する際には、最適化を施した結果のプログラムの性能について云々するべきであって、最適化を施さない稚拙な処理系による場合の性能について云々しても意味はない。
 手続き型言語プログラムに対する最適化とは、その言語によって記述された手続きを、同じ効果をもつ等価な手続きで、より効率的なものに変換することと認識できる。これは手続きから手続きへの直接的な変換であり、計算機の動作に対応付けて語られる手続き型言語の意味論の中に閉じた変換であるといえる。
 一方、本来手続きを書き表すものではない宣言型言語プログラムに対して「最適化」という言葉を用いる場合、その意味合いはかなり違ってこざるを得ない。宣言型言語に対しても手続き的な意味論を与える場合があるが、それは意味論の根幹となっている形式体系によっては説明しがたい付加的な機能に対して意味づけするなどの目的のためであり、本来の言語の性質からすれば補助的な役割を果たすものにすぎない。宣言型言語プログラムに対する「最適化」を云々する場合は、実はなんらかの標準的な言語実装方式を仮定し、その方式による処理手続きよりも効率的な実装を行うことを指すと考えられる。

2.4 手続き型言語に対する最適化技術の進展
 最近の手続き型言語プログラムの最適化系は、前述したような手続きから手続きへの変換という最適化を、直接的に行っているわけではない。機械語にして数命令程度の範囲でのいわゆるピープホール最適化については、あらゆる場合を尽くした探索もそれほど大きな手間をかけずに出来るため、機械語命令列から機械語命令列への直接的な変換が十分可能である。しかし、より大きな範囲、たとえば手続きひとつ程度、機械語命令にして百命令オーダ程度以上になると、探索空間が広くなり過ぎ、記述を機械語よりは若干高い抽象レベルにおいても、このような手続きから手続きへの直接的な変換は計算量的に困難である。そこで、手続きとして記述されたプログラムを、いったん抽象的な意味領域に写像し、その領域上での表現が意味するところを、コード列という手続きの領域になるべく効率的になるように写像し直す、というアプローチが普通になっている。こうした抽象意味領域としてはデータフローグラフなどが用いられることが多い。
 これを自然言語に対する翻訳の場合にたとえていうなら、手続き型言語プログラムに対する素朴な機械語生成は逐語訳に、手続きから手続きへという最適化は直訳に、いったん抽象意味領域に写像してからのコード生成は意訳に対応するといえよう。



 データフローグラフは、手続き型言語プログラムに記述されていた多くの手続き的詳細を捨象している。これに基づいて手続きを再構成することによって、手続き間の直接的な変換では難しかったさまざまな最適化が可能になる。
 たとえば、データフローグラフには実行順という概念はなく、データの依存関係があるだけである。もちろんこのデータ依存関係は元のプログラム中の実行順記述をもとに抽出したものであるが、プログラム中に記述した実行順はデータフロー依存関係にどのように影響するかを解釈された後は無視される。このことによって、実行順制約は緩和され、対象とする計算機の性質に依存して実行順を入れ換える最適化、たとえばパイプラインストールが生じにくい実行順の選択などが、自然な形で可能になってくる。共通部分式の括り出しや、さらにはコンパイル時に静的部分評価を行うなども、実行順についての自由度増大を一般化したものと見ることができる。
 また、データフローグラフには元のプログラムにあった記憶場所の名前としての変数という概念はなくなっている。計算の途中結果である値というものがあるだけで、これをどこにどのような形で記憶しておくかは、データフローグラフを実現するコードの生成法に依存している。プログラム中のひとつの変数には、実行の状況によってさまざまな値が入る。それぞれの状況における変数値をどこに記憶するかは、データフローグラフを元に考える限りにおいては、まったく独立した問題であり、元のプログラムで同一の変数に格納されていたということは特段の影響を与えない。このため、プログラム中の同一の変数を、あるときはレジスタ上、あるときはメモリ中など、効率上最適な場所に配置することが自然に可能になる。さらに、変数に入っている値をこれ以上使わないという状況では、その変数の値はどこにも記憶していないという状況も起こりうる。
 こうした最適化は手続きから手続きへの変換という考え方によって不可能というわけではない。実際、ごく低レベルの機械語命令列としてしかプログラムを認識できないCPUの中で、上述の最適化はそれぞれout-of-order実行、register renamingといった言葉で知られる最適化として、ハードウェアレベルで実現されている。こうしたハードウェアによる最適化は、どのようなキャッシュミスが生じたかなどといった実行時にしか得にくい情報に基づく最適化が可能である利点はあるが、ハードウェア資源の制約上ごく狭い範囲のみに注目したピープホールの最適化しか行なうことができず、その効果はおのずと限定されている。近年の最適化コンパイラが行うような、ある程度広範囲にわたる最適化を施すには、いったんデータフローグラフのような高い抽象度のレベルに写像した方が、見通しのよいコード生成を整合性よく行うのに有利になっているのである。

3. 宣言型と手続き型のギャップ縮小
 前節に述べたような手続き型言語に対する高度な最適化技術の進展を見ると、手続き型言語の宣言型言語に対する性能面での優位性の根源であった、計算機ハードウェアとの直接的な関係は崩れつつあるといえる。
 宣言型言語はもともと計算機のハードウェアとは遠い形式的体系でその意味が与えられるため、実際の計算機の持つ機能との間には小さからぬギャップがあった。一方、手続き型言語の場合このギャップはなきに等しく、プログラム中に用いられた言語機能を逐語訳するような形で機械語に翻訳することができた。ところが、前述したとおり、効率性を追求するための最適化を施すにあたっては、この言語の持つ機能と計算機のハードウェア機能の対応関係を直接的に利用するよりも、いったんデータフローグラフのような抽象度の高いレベルに持ち上げた方が有利なのである。
 こうしてみると、手続き型言語で記述するのは手続きそのものではなく、手続きという形式を借りて解くべき問題を記述しているのだ、とも見ることができる。たとえば C 言語ならば、四則演算や論理演算を行える演算装置、関数やブロックに入る際に自動的に拡張され名前文字列と関係付けられるようなメモリ装置、if-then-else, while, switchなどのような実行制御装置を備えた仮想機械を考え、その仮想機械上でどのように計算を行うかを記述する。しかし、この記述は実際の計算手続きの記述ではなく、プログラムに記述した手続きを仮想機械上で実行した場合に得られる結果という形式で、結果として得るべきものを指定する記述と考えるわけである。言語処理系はプログラムを解析してどのような結果を得ればよいのかを解釈し、実際の計算手続きを決めるにあたっては問題記述に用いた手続きはいったん忘れ、指定された結果を得るためにもっとも効率的な手続きを、別途ゼロから組み立て直す、という考え方である。
 データフローグラフは計算機ハードウェアと直接対応する形式でないとはいえ、宣言型言語の基礎となっている形式体系、たとえば論理型言語の基礎である一階述語論理に比べれば、はるかに計算機のハードウェアに近い表現である。そうではあっても、手続き型言語が計算機ハードウェアによる処理と一対一に直接的に対応するという考え方は、既に成り立たなくなっていると考えられる。
 従来の手続き型言語に対する最適化処理は、主としてひとつの手続きの中に閉じた解析・最適化であり、手続き間にわたる広域的な最適化は現在のところ比較的限定されたものでしかない。今後、ある程度大規模なソフトウェアシステム全体にわたるような最適化を行う場合、現在よく用いられているデータフローグラフのようなレベルよりも、さらに高い抽象レベルの表現に依らなければ、妥当なコスト範囲での解析が実現できなくなる可能性が高い。このことを考えると、遠くない将来、宣言型言語と手続き型言語の差異は、計算機ハードウェアとの疎遠によってではなく、問題記述の方法論の違いという視点から語られることとなり、手続き型言語は仮想的な抽象機械上での「手続き」という形式体系にのっとった宣言型言語の一種である、とみなされるようになるものとも予測できる。

 

【次へ】