1. Pascalとソフトウェア工学
最近、ほかの人の競技プログラミングの解答を見ていたら、PascalでCと同等の実行速度の回答があり、びっくりしました。
たとえば、累積和を求めるプログラムは
program RangeSum;
const
nn = 300 * 1000;
var
n, q, i, l, r: int32;
a, prefix: array [1 .. nn] of int64;
InputBuf, OutputBuf: array [1 .. 65536] of Char;
begin
SetTextBuf(Input, InputBuf);
SetTextBuf(Output, OutputBuf);
readln(n, q);
for i := 1 to n do
readln(a[i]);
prefix[0] := 0;
for i := 1 to n do
prefix[i] := prefix[i - 1] + a[i];
for i := 1 to q do begin
readln(l, r);
writeln(prefix[r] - prefix[l - 1]);
end;
end.Code language: Delphi (delphi)
というのも、Pascalは、C言語よりは「抽象的」な言語で、C言語ほどは高速化できないのでは、思っていたからです。
しかし、Pascalは、境界チェックをはじめとした安全機能の多くはコンパイラの最適化フェーズで除去できます。
つまり、実行時のオーバーヘッドを必ずしも伴わないため、高速に処理できるんですね。
その Pascalを設計した Niklaus Wirthは、「ソフトウェア工学小史」の中で、C言語の普及がソフトウェア工学を「大後退(great leap backward)させた、と語っています1。
From the point of view of software engineering, the rapid spread of C represented a great leap backward. It revealed that the community at large had hardly grasped the true meaning of the term “high-level language” which became an ill-understood buzzword.
A Brief History of Software Engineering – Niklaus Wirth
What, if anything, was to be “high-level”?
しかし、彼の思いとは裏腹に、世の中に普及したのは Pascal ではなく、C言語でした。
ここには、「ソフトウェア」とはどのようなものなのか、それが社会にどのように受け入れられてきたのか、ということを考える手がかりがあります。
1.1. 「手工芸」を「工学」にする試み
もともと、プログラミングは「探索」でした。
難解なコードとトリックを愛する職人たちが、問題を探索しながら形を見つけていく「craft(手工芸)」です。
経験と勘で解き、そこには再現性も体系もなかった。
これでは、規模の大きなプロジェクトになると、手に負えなくなってきます。
この問題を最初に大きく取り上げたものの一つに、1968年のNATO会議が上げられます。
1968年、西ドイツのガルミッシュで開かれたNATO主催の会議では、「ソフトウェア危機」と「ソフトウェア工学(Software Engeneering)」いう言葉が生まれました2。
IBMのOS/360をはじめ、当時の大型システム開発は軒並み遅延と品質不良に陥っていた、という問題を抱えていて3、Dijkstraは「規律ある工学にしなければならない」として「構造化プログラミング」を提唱します。
その当時のもっとも代表的な問題は、GOTO文と「スパゲッティコード」でしたが、それに限らず、ソフトウェア開発を「工学的な手法」で体系化することを目指す考え方が生まれ、さまざまなプログラミング言語や開発手法が生まれる起点となりました。
2. PascalとC、複雑さへの2つの答え
Wirthは、この構造化プログラミングの考え方で、プログラミング言語 Pascal を作りました。
誤りを書きにくい言語にする。
抽象化してハードウェアの詳細を隠す。
それがソフトウェアを工学にする道だ、と。
いわば「言語が正しさを強制することで複雑さを制御する」という考えです。
一方、Cも複雑なシステムをどう解決するか、という同じ問題への解として生まれています。
ベル研究所でCやUNIXが生まれた理由の一つは、Multicsという巨大プロジェクトの失敗の反動です。
Ken ThompsonがMulticsから撤退した後、一人で小さなシステムを書き始めたのがUnixの起源です4。
Multicsで使われていたBCPL(Basic Combined Programming Language)をもとに、簡略化した B言語をThompsonが作り、その後Dennis RitchieがBに型システムを追加し、コンパイラを機械語を直接出力する形に作り直したものがCです5。
CとUnixは「小さくシンプルにすることで複雑さをそもそも回避する」という答えです。
つまり、PascalとC、この二つは対立する思想から生まれたのではなく、「複雑化するシステムプログラムをどう作り切るか」という同じ問題への異なる解として生まれました。
2.1. Cはソフトウェア工学的には「後退」なのか?
しかし、Cの台頭は、Wirthにとって「大後退(great leap backward)」に映りました。
それは、C言語は、抽象化が不完全であったことです。
- 配列の境界チェックなし。
- 型の整合性チェックなし。
- ポインタは実質的にアドレス演算。
Cでは、 配列の範囲外に書き込んでもコンパイルは通ってしまいます。
int arr[5];
arr[10] = 42; // 未定義動作。実行時に何が起きるかわからないCode language: Arduino (arduino)
問題が発覚するのは、実行時です。
一方、Pascalでは、明示的な範囲外アクセスはコンパイルエラーになり、実行できません。
var arr: array[0..4] of Integer;
arr[10] := 42; { コンパイラが拒否する }Code language: PHP (php)
ソフトウェア工学は、「間違いようのない言語設計」を進めてきましたが、その対極にある C がソフトウェア開発の基盤となったのです6。
3. Cの与える「速度」
Cが提供したのは、さまざまな側面での速度です。
- 実行速度(ハードウェアに近い)
- 開発速度(簡潔な記法)
- 移植速度(小さなコンパイラ)
実行速度はわかりやすいです。
ハードウェアに近い分、余計な抽象化レイヤーがない。
Pascalの境界チェックやAdaの型検査は安全ですが、サイクルを消費します。
Cはそれを省きました7。
開発速度も大きかった。
型システムが緩いことで、汎用処理を書く制限がありません。
Pascalのように配列サイズを型の一部すれば誤りを書きにくくなりますが、その代わりに、動的な配列の汎用処理なども同時に書きにくくなってしまいます8。
移植速度は見落とされがちですが、構造的には最も重要かもしれません。
Cのコンパイラはとにかく小さく、文法が単純で、ランタイムがほぼない。
新しいCPUアーキテクチャが出たとき、Cコンパイラを移植するコストが他言語より圧倒的に低かった。
結果として、新しいハードウェアが出るたびにCが最初に動く言語になりました9。
最初に動く言語が標準になる。
UNIXとの共進化がそのサイクルを決定的にしました。
3.1. 傲慢さと怠惰さを同時に満たした言語
Pascalのコードを読んでみると、その美しさには惹かれる部分があります。
ただ、実際に書いてみると、窮屈さを感じます。
処理とは直接関係ない宣言が多く、また変数宣言と初期化が分離しているため、コードの意図を追いにくい。
形式が正確であるがゆえに、コード全体の見通しが悪くなるという逆説があります。
結局、Pascalで書いても、複雑な問題は管理しにくいコードになってしまいます。
「それならCの方がまだ素直だ」と思うのは、多くのプログラマが辿った道だったのではないかと思います。
プログラマには二つの相反する欲求があります。
- 一つは「すべてを制御したい」という欲求(傲慢)。
- 一つは「雑多な作業は言語に任せたい」という欲求(怠惰)。
ハードウェアの性能を最大限引き出すには、メモリのどこに何があるかを把握し、キャッシュの動作を意識し、割り込みを直接触りたい。
抽象化はその視界を曇らせます。
// メモリアドレスを直接計算して読み書きできる
uint32_t *reg = (uint32_t *)0x40021000; // ハードウェアレジスタのアドレス
*reg |= (1 << 3); // 特定のビットを立てるCode language: Arduino (arduino)
また、自明な宣言を毎回書きたくない。
自明な処理は自動化されていてほしい。
Wirthから見れば「抽象化の責任を言語が持つべき」ですが、プログラマからすれば「その責任は自分が持つ、だから権限もくれ」という話でした。
個人開発でも企業開発でも、ビジネス領域のあらゆるフロンティアで使われた理由はここにあります10。
4. 正しさよりも速度という時代
Wirthの「正しさ」を重視する考え方は、特定の領域では今も正しいです。
航空、軍事、医療機器——ミスが人命に直結する領域では、言語が正しさを強制することに本物の価値があるからです。
米国防総省の標準的なプログラミング言語は、CではなくAdaです11。
これは、Pascalをさらに厳格にしたプログラミング言語と言えます。
フライトコントロールシステムでバッファオーバーフローが起きれば、それは障害ではなく惨事になります。
しかし、世界で多くのプログラマが従事しているのは、そういった領域ではありません。
1960年代から2000年代にかけて、システム開発の領域は大きく広がりました。
受発注システム、ECサイト、社内ツール、スタートアップのプロダクトといったビジネスの現場です。
この分野ではミスはコストですが、それを上回る利益があれば相殺できます。
そして、ソフトウェアビジネスでは、とにかく一番目に作ることに価値が集中します。
4.1. ソフトウェアは複製できる
1960〜2000年代にかけて、人類は「ソフトウェア」という媒体の本質を、まだ理解していなかった、と言えます。
Wirthをはじめ当時の人々が「ソフトウェアを工学にしなければ」と言ったとき、頭の中には建築や機械工学のアナロジーがありました。
設計して、作って、検査して、出荷する。
建築では同じ設計で二棟目、百棟目、と作ることに意味があります。
しかし、ソフトウェアは、そういうものではありませんでした。
その本質は、ビジネスの現場で事後的に発見されていきました。
- 1つ目は、スケーラビリティです。
物理的な製品は地域や流通で市場が分割されますが、ソフトウェアはコピーコストがゼロなので、最良のものが世界中に瞬時に広がります。
コピーがゼロコストで複製できるため、同じものを二度作る理由がなく、市場で二番目になった瞬間、そのソフトウェアが存在する理由の大半が消えます12。
今でこそ当たり前ですが、このような現象は、Microsoft、Yahoo、Google、Amazonといった企業が、スタートアップからいっきに大企業へと変貌する歴史を目撃したからこそ言えることです。 - 2つ目は、ネットワーク効果です。
使う人が増えるほど価値が上がる。
これも物理製品にはない性質で、勝者総取りをさらに加速させました。 - 3つ目は、アップデートによる後追い修正です。
物理製品は出荷後に直せませんが、インターネットに接続したソフトウェアは出荷後に書き換えられます。
これは、品質基準を根本から変えました。
「完璧に作ってから出す」より「出してから直す」が合理的になる。
つまり、ソフトウェアには、建築などの従来の工学と大きな違いがあります。
軍事・航空では「出してから直す」は許されません。
ビジネスの世界での成功においては、「出してから直す」が戦略になります。
この非対称性は、1990年代になって初めて広く理解されるようになりました。
「正しく作る」より「最初に作る」が重要だったということは大きな誤算だったとも言えます。
4.2. 標準化と自由の振り子
この歴史を振り返ると、ソフトウェア作りをどういう行為と見なすかという点で、2つの立場が並立していたことがわかります。
- 一つは、設計が先にあり、コードはその実装で、プロセスを工学にする、という発想です。
「どう作るか」を言語で標準化して、チーム開発の中で強制する方向性です。
Pascalだけでなく、JavaやPythonなどにはその傾向を感じます。 - もう一つは、制約より自由を選ぶ考え方です。
プログラマは「どう作るか」を自分で決め、「何を作るか」の探索を言語に妨げさせない。
C言語だけでなく、C++、PerlやJavaScript、Common Lispなどもその方向性です。
2004年にPaul Grahamが『ハッカーと画家』では、プログラミングは工学より絵画や建築に近い。コードを書くことが設計であり、探索である。「何を作るか」を高速に発見することこそが優れたソフトウェアを生む、とあります。
彼がLispを愛し、スタートアップでそれを実証したのは偶然ではありません13。
Wirthの「ソフトウェア工学においてCは後退だったのか」という問いを振り返ると、ソフトウェアが展開していったビジネスの世界では、「ソフトウェア工学」という前提が成立しなかった、ととらえることができます。
ソフトウェア工学では「設計して、検査して、出荷する」という建築的なモデルを前提にしていましたが、コピーコストがゼロで、出荷後に書き換えられ、最初に届けた者が市場を独占するという媒体に、そのモデルは合いませんでした。
プログラミング言語は、工学的な正しさによって普及するわけではありません。
ビジネスが要求した速度と自由に、Cだけが応えられたから、広く使われたのです。
Wirthの問題意識は正しいのですが、彼が解こうとしたのは、ビジネスが直面していた問題とは別の問題だったと整理できます。
5. 【補足】「よりよいC」という模索
ちなみに、約半世紀ほどして、Cの欠陥が無視できなくなった領域でようやく別の選択肢が現れてきました。
2015年に安定版 1.0 がリリースされた Rustです。
Cの欠陥は早くから知られ長らく、「よりよいC言語」は模索されてきましたが、明確な代替案は誰も提示できませんでした。。
たとえば、C++は、Cの上にオブジェクト指向を乗せましたが、Cの危険な機能はすべて引き継いでいます。
互換性を守ることが安全性より優先され、言語設計者の Bjarne Stroustrup自身が後に「C++を安全に使うにはサブセットを決めて残りを使わないようにするしかない」と述べるくらい、「危険で複雑な言語」になりました。
Javaはガベージコレクタでメモリ安全性を得ましたが、システムプログラミングに必要な低レベルの制御は手放しました。
Cが独占していた領域——ハードウェアに近く、実行速度を要求され、OSやドライバやシステムソフトウェアを書く場所——には、50年近くにわたって本質的な代替手段が存在しませんでした。
ただ、Cの「間違いを許容する」問題は、徐々に無視できないものになりました。
Microsoftは2004年からCVEをトリアージした結果として、C/C++のメモリエラーが全脆弱性の約70%を占めていたと報告しました14。
5.1. Cのコストが無視できなくなった(Rust)
Cのコストが臨界点を超えたとき、Rustが登場しました15。
Rustは、アフィン型や線形型と呼ばれる、型理論の研究の発展を前提としています。
Rustは、Cが実現した「制御の自由」を手放さないまま、コンパイル時にメモリ安全性を保証する、というやり方を取りました。
// C: 解放済みメモリへのアクセスはコンパイルが通る
char *p = malloc(10);
free(p);
p[0] = 'a'; // 未定義動作Code language: Arduino (arduino)
// Rust: 同じことをしようとするとコンパイルエラー
let s = String::from("hello");
drop(s);
println!("{}", s); // error: borrow of moved valueCode language: Rust (rust)
所有権システムによって、プログラマが「何を隠すか」を決める自由を保ちつつ、その決定が誤っていればコンパイラが通さない16。
Googleは、AndroidでRustを採用した結果、メモリ関連のバグが2019年の76%から2024年には24%まで低下したと発表しました17。
Cが何十年も独占していた場所に、初めて別の選択肢が根付きつつあります。
- Wirthによる批判の全文は2008年の論文「A Brief History of Software Engineering」に収録されている。Cへの批判の章で彼はこう述べる。「CはUNIXのためのTrojan horse(トロイの木馬)として機能した」。 – A Brief History of Software Engineering – Niklaus Wirth
- 会議はPeter NaurとBrian Randellが編集した231ページの報告書としてまとめられた。「ソフトウェアエンジニアリング」という言葉は、当時の慣習に意図的に挑戦する言葉として選ばれた。 – NATO Software Engineering Conferences – Wikipedia
- OS/360はFred Brooksが指揮した。1966年の最初のリリースは「遅れ、予定より多くのメモリを使用し、コストは見積もりの数倍、最初のバージョンのパフォーマンスは良くなかった」とBrooks自身が後に記している。その経験は後に『The Mythical Man-Month』(1975年)にまとめられた。 – The Mythical Man-Month at 50 – Kieran Potts
- Bell LabsはMIT・GE・AT&Tが共同で進めていたMulticsプロジェクトから1969年に撤退した。その後Thompson一人がPDP-7上でゲームを動かすための小さなOSを書き始め、それがUnixになった。「大きく正しく」への反動として「小さく動く」が生まれた典型的な経緯。 – Dennis Ritchie – Wikipedia
- BCPL→B→Cという系譜はDennis Ritchie自身の論文「The Development of the C Language」(1993年)に詳述されている。BはBCPLの型なし設計を引き継いでいたが、PDP-11がASCII文字処理を重視するハードウェアだったことが型システム導入の直接の契機になった。1973年までにUnixカーネル自体がCで書き直された。 – The Development of the C Language – Dennis Ritchie
- Wirthによる批判の全文は2008年の論文「A Brief History of Software Engineering」に収録されている。Cへの批判の章で彼はこう述べる。「CはUNIXのためのTrojan horse(トロイの木馬)として機能した」。 – A Brief History of Software Engineering – Niklaus Wirth
- FreeBSDの開発者Poul-Henning Kampは、Cのnull終端文字列がPascalの長さ付き文字列に勝ったことを「史上最も高くついた1バイトのミス」と呼んだ。安全性より実装の簡便さを選んだCの設計思想を象徴するエピソードのひとつ。 – Comparison of Pascal and C – Wikipedia
- BellLabsのBrian Kernighanは1981年に「Why Pascal is Not My Favorite Programming Language」を執筆し、Pascalの実用上の問題を具体的に列挙した。配列サイズが型の一部であるため汎用ルーチンが書けない、文字列処理が困難、static変数が使えないといった制約が、現場での使いにくさの根拠だった。 – Why Pascal is Not My Favorite Programming Language – Brian Kernighan
- 1973年にUnixカーネル自体がCで書き直された。これにより、新しいハードウェアへの移植はアセンブリを手作業で翻訳する代わりにCコードを再コンパイルするだけで済むようになった。Dennis RitchieとStephen C. JohnsonはPortable C Compilerを開発し、Cの移植性をさらに高めた。 – C (programming language) – Wikipedia
- Hacker Newsの「Ask HN: Why Did Pascal Fail?」スレッドでは、「CがUnixとWindowsの公式言語になった時点でPascalに勝ち目はなくなった」という見方が多数を占めた。言語の優劣より生態系の政治が普及を決めた典型例として語られている。 – Ask HN: Why Did Pascal Fail? – Hacker News
- Hacker Newsの議論では「安全クリティカルなプロジェクトでは、C/C++はAdaより親しみやすい構文という理由だけで採用され続け、コーディング標準とlinterの組み合わせで安全性を担保しようとしていた」という指摘がある。つまりC++自体を安全に使うには、C++でないものを作るしかなかった。 – Going Beyond Ada 2022 – Hacker News
- Brooksはソフトウェアプロジェクトの60〜80%が遅延すると指摘した。時間的プレッシャーがいかに常態化していたかを示す数字であり、「最初に届ける」ことへの圧力がいかに強かったかを裏付けている。 – Time Pressure in Software Engineering: A Systematic Review
- Paul Grahamの『ハッカーと画家』(2004年)はプログラミングをcraftおよびアートとして捉え直した書として知られる。Grahamは自身のスタートアップViaWebをLispで構築し、それが競合に対する優位の源泉だったと述べている
- MicrosoftのセキュリティエンジニアであるMatt Millerが2019年のBlueHat ILカンファレンスで発表した内容。2004年からMicrosoftセキュリティレスポンスセンターが集計したCVEの約70%がメモリ安全性に起因するバグだった。 – A proactive approach to more secure code – Microsoft MSRC Blog
- RustはMozillaのエンジニアGraydon Hoаreが2006年に個人プロジェクトとして開始した。エレベーターのソフトウェアがクラッシュして21階まで階段を上らなければならなかった経験が動機だったと本人が語っている。Mozillaが2009年に正式スポンサーとなり、2015年に安定版1.0がリリースされた。 – How Rust went from a side project to the world’s most-loved programming language – MIT Technology Review
- Rustの所有権システムはGarbage Collectorを持たずにメモリ安全性を保証する。2013年までにGarbage Collectorは所有権システムに置き換えられ、現在の設計が確立した。所有権システムの設計にはCycloneやML Kitといった研究言語の領域ベースのメモリ管理が影響を与えている。 – Rust (programming language) – Wikipedia
- Googleのセキュリティブログ「Eliminating Memory Safety Vulnerabilities at the Source」(2024年9月)による。Androidチームは2019年頃から新規開発をメモリ安全な言語に移行し始め、既存コードは書き直さず新規コードのみRustで書く戦略をとった。Rustの変更のロールバック率はC++の半分以下だったとも報告している。 – Eliminating Memory Safety Vulnerabilities at the Source – Google Security Blog