* セグメンテーションの夢はどうなったのか?
-(by [[K]], 2016.12.06)

** (0)
-この記事は、「自作OS Advent Calendar 2016」の12/06が空欄だったので急きょ書くことにした記事です。
--http://www.adventar.org/calendars/1666

** (1)
-「30日でできる!OS自作入門」の読者の人たちは、みんなページングに憧れます。・・・まあそうですよね、ほぼすべてのメジャーなOSが使っている仕組みですし、たいていのCPUに搭載されている機能ですし、本の中では説明されていませんし。
-それに対して、セグメンテーションは不評です。できるだけ使わないほうがいいとまでいう人がいます。悲しいです。まあ読者の中にはそこまで悪く言う人はいないかもしれませんが。
-今日はなぜ私が「悲しい」と感じるのか説明してみたいと思います。

-OS自作入門では、112ページにセグメンテーションの説明があります。そこでは ORG 0 で書かれたプログラムたちが衝突しないために導入されます。つまりメモリの任意の場所を0番地にするためです。

** (2)
-セグメンテーションが世間的に不評な理由を考えると、第一にそれは8086~80286のセグメンテーションが原因だと思われます。8086は1MBのメモリ空間を持っていましたが、アドレス指定に使えるレジスタは16ビットしかなく、つまり64KBしか使えないということでした。1MBを使い切るためには、そのたびにセグメントレジスタの値まで変更する必要があり、それはセグメンテーションが好きな私から見ても非常に使いにくい仕様でした。
-プログラムが100KBくらいのバッファを持つことはそう珍しいことではなく、64KBを超えたデータを扱うときはその都度セグメントレジスタを強く意識する必要があり、とても面倒だったのです。640x480x8ビットカラーをやろうと思ったらそれだけで300KBです。いかに64KBが小さいかわかるでしょう。
-80286ではメモリが16MBまで増えましたが、セグメントの最大サイズは相変わらず64KBであり、もう救いがたい状況でした。・・・しかしこれはセグメンテーションが悪いというよりは、レジスタ幅が16ビットなのが悪いのです。16ビットだから64KBまでしか簡単にアクセスできないというのはまあ自然なことであって、それをセグメンテーションのせいにするのはちょっと話が違うようにも思います。
-ちなみに8086のライバルとされたMC68000は、16ビットのCPUといわれながらレジスタ幅は32ビットあって、64KBの制約などなく自由にメモリの好きな場所へアクセスできたのです。
-ではMC68000が16ビットのCPUとしてはオーバースペックで、8086のほうが妥当なのかというと、私はそうは思いません。だって、Z80や6809は8ビットでしたが16ビットのレジスタを普通に持っていたからです。「16ビットCPUになったから32ビットのレジスタを持たなければならない」とまではいいませんが、8ビット時代を思えばMC68000みたいな仕様は不自然ではなかったのです。むしろ8086は不自然でした。
-まあ一説によると8086は8ビットCPUとの互換を意識しており、セグメンテーションでメモリを64KBごとに切り分けてそれぞれの中で8ビット時代の規模のプログラムが動くことを想定していたらしいので、そういう意味では8ビットCPUの感覚で作られたのかもしれません。それを80286まで引きずってしまったわけです。

-80386が出て、整数レジスタが32ビット化されると、セグメンテーションは不便なものから便利なものに変わります。4GBまではセグメントレジスタを持ち替えずとも連続してアクセスできるようになったからです。
-セグメンテーションがあれば、0番地から始まる領域を同時に複数使わせることができます。たとえばプログラムコード用、データ用、スタック用、共有メモリ用、共有ライブラリ用、などです。特に共有ライブラリではとても便利で、なぜならどの番地にロードされるか悩まなくていいからです。0番地だとあらかじめ決め打ちできます。
-それぞれのセグメントに対して細かい権限の設定も可能です。
-つまり32ビット化されて64KBの壁が無くなったおかけで、セグメンテーションは「メモリを切り分けて任意の場所を0番地にする」という本来の便利な使い方ができるようになったのですが、しかしそれまでの「セグメンテーションは悪」という印象に釣られて、全く使ってもらえない機能になったのです。

** (3)
-ページングについて考えてみましょう。ページングにはいろんな方式があるのですが、とりあえずここでは80386に搭載されていたページングについて説明します。
-ある4バイトにアクセスしたい場合、その4バイトがページをまたいでいないという理想的な場合を考えるとして、そうするとまずはCR3(コントロールレジスタ3)を参照して、PDE(ページ・ディレクトリ・エントリ)の中の1つを読みに行きます。これでPTEが決定します。次はPTE(ページ・テーブル・エントリ)の中の一つを読みます。これでどのページを使うかが決定します。これでメモリアドレスが確定するので、目的の4バイトにアクセスします。
-つまり4バイトにアクセスするだけなら本来はメモリアクセスは1回で済むはずなのに、余計に2回もメモリを読む必要があり、だからメモリアクセス回数が3倍になってしまうのです。これではCPUは遅くなってたまらないので、ページングのための専用キャッシュが用意されています。それをTLB(トランスレーション・ルックアサイド・バッファ)といいます。80386はL1キャッシュすらCPU内には内蔵しないCPUでしたが(昔は集積度が低くてL1キャッシュをCPU内に入れられなかったのです)、それでもTLBはちゃんとCPU内にありました。というのはTLBがなかったら信じられないほど遅くなり使い物にならないからです。
-80386では32本のTLBを持っていました。これがCPUの世代が増すごとに増強され、Nehalem/WestmereベースのCore iシリーズでは704本にまで増えます。つまりTLBがミスヒットしたらもう性能がガタ落ちなので、とにかく一度読んだところは絶対に忘れないようにしよう、という決意の表れです。

-しかし悲しいことがあります。
-TLBはタスクが切り替わったら(CR3が変わったら)クリアしなければいけないのです。つまりキャッシュは全部一度捨てることになるわけです。つまりタスクが切り替わるたびに性能はがた落ちで、タスク切り替え頻度を上げすぎると性能が大きく落ちてしまうことになります。

-これに対して、セグメンテーションはどうでしょうか。よくセグメンテーションは遅いと言われます。それはセグメントレジスタに値をロードするのが少し遅いとか、メモリアクセスの際にデフォルト以外のセグメントレジスタを指定すると1クロックのペナルティを課されるとか、そのことを言っているのでしょう。いえいえ、そんなのはページングの平均オーバーヘッドから見れば些細なものです。だからこれはいいがかりなのです(後述)。
-また、セグメンテーションはTLBのような大規模なキャッシュを必要としません。セグメントレジスタの設定値(GDTやLDTから読み込んだ値)を保持する隠しレジスタは持っていましたが、それは全部合わせても6本です。つまりハードウェア的にははるかに簡単な構造なのです。

** (4)
-80386は、そんなセグメンテーションとページングの両方を持っている素晴らしいCPUでした。つまり両方をONにすると、双方の長所を生かしたとてもすばらしいOSが作れるのですが(それが第一世代OSASKです!)、WindowsやLinuxではセグメンテーションは事実上無効化されて、ページングだけが酷使されました。そのためCPUが進化して64ビット対応した時には、64ビット環境ではセグメンテーション機能はサポートされないことになってしまいました。64ビットでセグメンテーションがもし使えたら、もっとすごいことができたはずなのに・・・。
-インテルの設計を見ていると、明らかにページングとセグメンテーションの両方を組み合わせて使うことを想定していたと思います。私はそう感じたからこそOSASKを作ったわけですし・・・。

-セグメンテーションを徹底的に使いにくくした元凶は、x86向け32ビットのC言語の普及だと思っています。それより以前のCコンパイラはセグメントレジスタを意識したプログラムも書けたのですが(だってそうじゃないと64KB超のメモリにアクセスできない!)、それ以降のCコンパイラではインラインアセンブラ以外ではセグメントレジスタが使えません。intの幅はポインタの幅と同じだぜー、そんなの当然だろ、それ以外の場合を想定するなんてバカなんじゃないの?という感じです(実際そのように言われました)。・・・でも64ビットの世界になるとintの幅とポインタの幅は同じじゃなくなって、結局は彼らが誤解していただけだったと歴史が証明するわけですが、しかしそのころにはセグメンテーションはすでに帰らぬ人になっていたのです・・・。

** (5)
-セグメンテーションとページングの両方を組み合わせた、第一世代OSASKがどうなったかを紹介しましょう。
-OSASKでは基本的にセグメンテーションによってメモリを切り分けます(厳密には分け合っているのはメモリ空間であってメモリではないですが)。だからタスクがいくつあってもメモリ空間は1つしか使いません。つまりCR3は一つしかないのです。これにより、どんなに頻繁にタスクスイッチしてもCR3が変わらないので、TLBはクリアされません。この方法では、すべてのアプリで使うメモリ空間の合計が4GBを超えられないわけですが、OSASKはアプリもデータもコンパクトだったので、この仕様で困ることがありませんでした。ページングを使っているので、mallocで1MBを要求しても実際には32KBしか使っていなければ、実メモリは32KBしか割り当てられません。・・・これによりメモリ効率はとてもよく、大変快適でした。
-そして競合するどの自作OSよりも''圧倒的に高速''で、「セグメンテーションを使うと遅い」という思い込みを完全に打ち砕いたのでした。

-もし64ビットでもセグメンテーションが使えたら、64ビットのメモリ空間を切り分けることになるので、全部のタスクを一つの仮想空間にまとめても問題が生じることはないでしょう(第一世代OSASKでは、1GBのメモリを要求するアプリが同時に5つは起動できないだろうという問題がありました)。だから遠慮なくこのアーキテクチャで行けるわけです。
-きっとすごく快適だろうなあ・・・。

** (6)
-こんなに素敵な機能があったのに、みんな使わなかった。・・・そりゃあ、CPUの設計者はいじけますよ。
-x86以外ではメジャーな機能ではなかったというのはその通りですが、でもx86自体は相当たくさん売れたわけです。だからx86専用でもいいから使えばよかったと私は思うんです。

-みなさん想像してください。あなたは画期的なアイデアを思いついて、それを自分のOSで実現します。しかしそんなのWindows/Linuxで使えない機能だから意味ないよとかいって相手にしてもらえないわけです。どんな気持ちになるでしょう・・・。まさにそれと同じ気持ちをインテルの設計者は感じたはずです。

~
-さすがの私も、いまさら「今からセグメンテーションを活用したOSを作れ!」とは言いません。でもインテルの人たちが8086の時に導入したセグメンテーションを80386で見事に進化させたことは大いに評価するべきですし、世間が偏見でそれを台無しにしたことも歴史の事実として知っておいてほしいです。
-それがインテルの人たちへのせめてものなぐさめになるでしょう。

** (7) おまけの余談
-http://www.adventar.org/calendars/1666 はご覧のとおり空きがいっぱいあります。だから全部埋めようという気持ちになりきれないのですが、でも埋まってきてあと少しになったら、頑張りたい気持ちになると思うんです。もし残り5マスとかになって、でも書き手が見つからなくて困ったら、私もまた何か書こうと思っています。
-ということで、協力者募集!

* こめんと欄
-私の誤解だったら申し訳ありませんが、単一のメモリ空間を共有する仕組みにおいては、各プロセスのデータセグメントを拡張できなくなり、特に32bit時代のシステムでは困りませんか?OSASKやはりぼてOSは事前に確保するスタイルでしたが、単純に大きな領域を要求するだけで、他のアプリが(メモリプレッシャーが高まる以前に)開始することすらできなくなるという迷惑な行為が出来てしまう気がします。 -- ''hiro4bbh'' SIZE(10){2018-03-18 (日) 17:53:11}
-その質問の答えはyesですね。つまり迷惑行為は成立します。でもそれは私は問題だとは思っていなくて、それはユーザがそういう迷惑なアプリを終了させてしまえばいいかなと思うのです。 -- [[K]] SIZE(10){2018-04-06 (金) 22:24:20}
-お返事ありがとうございます。このような欠点があるから、Intelは(相対的に欠点の少ない)ページングを選んだのかもしれませんね。そもそも、TLBをもっと賢くして、CR3と仮想アドレスのペアでキャッシュしてくれれば、CR3切り替えによるオーバーヘッドは回避できるような気はしますが。 -- ''hiro4bbh'' SIZE(10){2018-04-08 (日) 11:50:38}
-と思ったら、ASID (Address Space Identifier)のような概念が既に使われているようですね。 (^_^;) -- ''hiro4bbh'' SIZE(10){2018-04-08 (日) 11:59:39}

#comment


トップ   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS