はじめに
皆さん、こんにちは。11月へようこそ。皆さんが元気に過ごされているとうれしいです。
10月はとても忙しい1か月でした。ちょうどコミュニティ解答に大きな改善を加えて公開したところです。似た解答は1つしか表示されないように重複を排除し、並べ替えの新しいオプションや、コードで検索する機能も追加しました。C#を見てみると、さまざまなプログラミングの概念で絞り込む機能も加わっているのがわかるはずです。ビットシフトや再帰など、気になるものを使った解答を探せるようになりました。この機能はコミュニティウィークに、ほかのトラックにも展開する予定です。
でも、今は#12in23に集中しましょう。 10月はオブジェクト指向言語を探求する、興味深い1か月でしたが、今月はハードコアにいきます。 取り上げるのはアセンブリ言語、とくにMIPSアセンブリ、x86-64アセンブリ、WebAssemblyです。 いつもどおり、Erikがこれらの言語のどこが面白くユニークなのかを解説してくれます。
バッジ
いつもどおり、これらの言語の演習を5つ完了すると、Nibbly Novemberバッジを獲得できます。1年を通して取り組むYear-Longバッジもあり、多くの方がそれを目指して取り組んでいることと思います。そのバッジのために、完了してほしい注目の演習を5つ用意しました。次のとおりです:
- Pop Count:数値の中の1のビットを数える
- Grains:マスごとに倍になっていくチェスボード上の粒の数を計算する
- Resistor Color:抵抗のバンドの色を、対応する数値に変換する
- Rotational Cipher:回転暗号(いわゆるシーザー暗号)を実装する
- Nucleotide Count:DNAの文字列の中に各ヌクレオチドが何回現れるかを計算する
背景
なぜNibble Novemberという名前なのか?
ご存じかもしれませんが、1バイトは8ビットです。そして、1ニブルは4ビットです。英語のbyteのyをiに変えるとbiteになる、と考えると、この名前の意味が見えてきます。つまりnibbleとは、小さなbite、ひとかじりというわけです。
アセンブリ言語とは?
まずは基本から見ていきましょう。CPUは命令を実行します。たとえば「2つの数値を足す」「ビットを左にシフトする」といったものです。こうした命令は機械語命令と呼ばれ、特定のビット列にすぎません。プログラムを実行するとは、いわゆる「単純に」、CPUがこうしたビット列を処理して実行することです。
命令をビット列として直接書き下すのは面倒で、間違いも起きやすいため、KathleenとAndrew Donald Boothは、ずっとさかのぼる1947年に、機械語命令を表すための、もっと人間にやさしい言語を考案しました。こうして機械語命令を表す言語を、アセンブリ言語と呼びます。このアセンブリ言語は、アセンブラーによって機械語命令に変換されます。
アセンブリ言語の面白いところは、CPUアーキテクチャーに直接結びついていることです。ただし、通常はオペレーティングシステムには依存しません。
ちなみに、パンチカードを覚えているでしょうか? あの、ごく初期のコンピューターでプログラムを実行させるために使われていた大きなカードのことです。あれもアセンブリ言語の一種だったのです!
アセンブリ言語は、今使っているプログラミング言語とどう違うのか?
一番の違いは、アセンブリ言語がとても低レベルだという点です。これまで慣れ親しんできた抽象化の多くがありません。クラスやオブジェクトはどこにもありません。ループ? ジャンプを使って自分で書く必要があります。関数? ありません! アセンブリのコードを書くのは、私にとってとても身の引き締まる思いでした。モダンな言語がどれだけ開発を楽にしてくれているかに気づかされるからです。でも、アセンブリのコードを書くことはとても役にも立ちます。物事が実際にどう動いているのか、ずっとよくわかるようになるからです。
豆知識をひとつ。RollerCoaster Tycoonのソースコードの99%は、手書きのアセンブリコードでした! これは驚くべき偉業で、自分で少しアセンブリを書いてみると、そのすごさがさらにわかるはずです。
今でもアセンブリコードを書く人はいるのか?
はい、昔ほどではありません。以前は、手書きのアセンブリがコンパイラーが生成した機械語を上回ることもよくありました(C++でアセンブリコードを直接埋め込めるのは、それが理由のひとつです)。しかし、コンパイラーは機械語の生成がとても上手になったので、今ではそんなことはほとんどありません。とはいえ、性能が重視される場面やリソースが限られた環境では、今でもアセンブリ言語が使われています。
概要
MIPS
- MIPS(Microprocessor without Interlocked Pipelined Stages)は、縮小命令セットコンピューター(RISC)命令セットアーキテクチャーのファミリーです
- MIPS Computer Systemsが開発し、1985年に最初のバージョンがリリースされました
- 複数のバージョンがあります:MIPS I、II、III、IV、V、MIPS32/64。最初の2つのバージョンは32ビットのみでしたが、MIPS IIIで64ビットがサポートされました
- SIMD命令や圧縮など、いくつかのオプションの拡張があります
- 後のRISCアーキテクチャーに大きな影響を与えました
- MIPSは2021年、MIPSアーキテクチャーの開発を終了し、RISC-V(オープンソースでロイヤルティフリーのアーキテクチャー)へ移行したと発表しました
- 主に組み込みシステム(ルーターなど)やサーバーで使われています(Silicon Graphicsのコンピューターが採用し、映画の特殊効果(SFX)で広く知られていました)。ほかにもNECのスーパーコンピューターCenju-4、TeslaのModel S、NASAの探査機New Horizonsなどで使われたほか、大学でアセンブリを教えるのにも、いくつかのゲーム機(初代PlayStation、PlayStation Portable、Nintendo 64など)でも使われています
x86-64アセンブリ
- AMDが設計し、1999年にAMD64アーキテクチャーとしてリリースされました
- x86命令セットの64ビット版です。x86の歴史は1978年にIntelが8086マイクロプロセッサーを発売したことにさかのぼります。これは16ビットのプロセッサーでしたが、その後80386で32ビット命令が追加され、その命令セットがx86と同義になりました
- 64ビットで実現できた最大のことは、より多くのメモリーを扱えるようになったことです(32ビットのアドレス指定は4GBまで)。これはボトルネックになっていました。64ビットは理論上、16エクサバイトまでアドレス指定できますが、現在は48ビットしか使われておらず、256TBまでです(必要になれば後で拡張できます)
- AMD64はx86命令セットを拡張したもので、互換モードを通じて既存の16ビット・32ビットアプリケーションと完全な互換性を持つように設計されました
- IntelはAMDを交えずにIA-64を設計していました。それは新しく、まったく異なる、後方互換性のない64ビット命令セットでした。最終的にAMD64が勝ち、Intelは独自のバージョンを実装しました(意味の違いはわずかです)
- あらゆるところで使われています。ワークステーションからサーバー(スーパーコンピューターを含む)まで、組み込みシステムからゲーム機(PS5やXbox Series Xなど)まで
WebAssembly
- Web技術の標準化団体であるW3Cが設計しました
- 設計目標は次のとおりです:
- 高速で、安全で、ポータブル
- 効率的でポータブルな表現
- 昔は、Web上で高速に実行するには、FlashやSilverlightのような専用のブラウザープラグインを使うのが一般的でした。JavaScript自体が高性能な計算にあまり向いていなかったからです。これらのプラグインの大きな欠点は、セキュリティー上の問題が多く、標準化されていなかったことでした。
- Mozillaはasm.jsを設計しました。これはJavaScriptのサブセットで、ブラウザー上で優れた性能でコードを実行できるようにすることを目指したものです。型の一貫性(型が動的に変わらない)とガベージコレクションなしによってそれを実現しました。言語はasm.jsにコンパイルすれば、Web上でもよい性能が得られました。ただし、あくまでJSだったので、できることには限りがありました。そこで提案されたのが新しい言語、WASMです。
- 実行する命令のセットを提供するという点で、アセンブリに似た言語です。重要なのは、特定のCPUに縛られていないことです。つまりプラットフォームに依存せず、プラットフォームごとに実装(仮想マシン)が必要です。このためWebAssemblyは、実は機械語ではなくバイトコードです
- 静的型付けです(JSとの決定的な違いです)
- 通常は事前コンパイルまたは実行時(JIT)コンパイルを使います(解釈実行も可能です)
- オープン標準で、2つのものを定義しています:
- バイナリーフォーマット
- テキストフォーマット(バイナリーフォーマットにコンパイルされます)
- 主要なブラウザーすべてに実装があります
- 高性能が求められる多くのWebページで使われています。Google Earth、Figma、Unity、Autocadなどです。サーバーサイドでも広がっており、たとえばマイクロサービスの実行、SaaSプラットフォーム(CloudFlare workersなど)やDocker上での実行に使われています
プログラミングの視点から見ると、どう違うのか?
MIPS
- ロードストアアーキテクチャー(別名レジスタ間アーキテクチャー)を採用しています。命令はメモリーアクセスか演算のどちらかを行いますが、操作するのはレジスタ内のデータだけです
x86-64アセンブリ
- レジスタ・メモリーアーキテクチャーを採用しており、レジスタだけでなくメモリー上の(またはメモリーからの)データに対しても演算を行えます
WebAssembly
- スタックベースのプログラミング(レジスタなし)を採用し、メモリーとのデータの読み書きもできます
これらの言語のどこが優れているのか?
MIPS
- 小さい。MIPS命令セットは、すべての命令が1ページに収まります
- 確立された呼び出し規約があるので、使えるレジスタをどう使うかがわかります。たとえば、引数を渡すのにどれを使い、結果を返すのにどれを使うか、といったことです。
- 安定している。最後のバージョンは2014年にリリースされました
- 広く文書化されている。とくに学術書で
- 実世界での使用例が豊富。何十億ものデバイスで使われています
x86-64アセンブリ
- x64の拡張でありながら、多くの新機能が追加されています:
- 64ビット整数のサポート
- 追加のレジスタ
- SSE命令(ベクトル命令)
- 相対データアクセス(共有ライブラリーを使うときに効率的)
- ノーエグゼキュートビット(メモリーの特定のページでコードを実行できないようにするセキュリティー機能)
- なじみやすい。x86命令セットを拡張したものなので、x86命令セットに慣れている人なら比較的すぐ学べるでしょう ドキュメントも豊富で詳細です
- 安定している。新しいバージョンが定期的に追加されていますが、中核部分はきわめて安定しており、後方互換性があります
WebAssembly
- スタックベースのWebAssembly仮想マシンは、物理プロセッサー(RISCを含む)のアセンブリ言語と比べて、軽量でシンプルです。そのため、コンパイル先として比較的扱いやすいのです。
- WebAssemblyのテキストフォーマットはS式の「シンタックスシュガー」を使って、なじみのある命令型のスタイルを実現し、それがスタックベースのコードに変換されます。S式を使った形式は「シュガー形式」と呼ばれ、バイナリーの中身と等価なもう一方の形式へ「脱糖」されます。S式は、LISPを触ったことがある人にはおなじみでしょう
- JavaScriptとの相互運用性が高い。JavaScriptとのデータのやり取りが簡単です。ただし重要な注意点として、WASMは(まだ)DOMを操作できません
- 継続的に改善されています。WASMの仮想マシンだけでなく、標準自体も活発に開発が進んでいます。SIMD関連の命令、ガベージコレクション、スレッド、末尾呼び出しの最適化など、たくさんの新機能が設計・開発されています
- 安全。コードは検証され、サンドボックス化された環境で実行されます。JavaScriptやネイティブのアセンブリ言語と比べて、静的検証の度合いが高いのです。意味論が明確に定義されているので、検証しやすく、筋道立てて考えられます
際立った特徴
MIPS
- 効率。MIPSプロセッサーはとても効率がよく、組み込みシステムに最適です。
- 性能。優れた性能を持ち、そのためスーパーコンピューターにも採用されました
- 学びやすい。命令が少なく、それぞれの命令が1つの単純なことしか行わないので、覚えやすいのです。教育用途にも最適です。
x86-64アセンブリ
- 強力。x86-64は何十年にもわたって改良されており、性能を高めるための命令がたくさんあります。その例がSIMD(Single Instruction, Multiple Data)で、これは、まあ、1つの命令を複数のデータに対して並行して実行できる命令です。
- どこにでもある。x86-64で動くデバイスはあらゆるところにあります。IntelとAMDのCPUがこれを実装しています。長い間、事実上の標準でした
- 定期的に更新されている。たとえばSSE3〜5、AVX、AVX-512などの新しいベクトル命令です
WebAssembly
- 効率的。バイナリーフォーマットはコンパクトで、高速なシングルパスでデコード・検証・コンパイルできます。ストリーミングも可能なので、すべてのデータを受け取る前に、デコード・検証・コンパイルをできるだけ早く始められます。並列化もできます。高性能なWebアプリケーションに最適です。
- 優れたコンパイル先。多くの言語のコードをWeb上で動かせます。主要な言語のほとんどがWebAssemblyバイナリーへのコンパイルをサポートしているので、JavaScriptを書かなくても自分のコードをWeb上で動かせます。言語によっては、コードではなくランタイムをWebAssemblyにコンパイルし、そこで変更なしのバイトコードを実行するものもあります。
- デプロイと実行が簡単。バイトコードを解釈できる仮想マシンがあればよく、主要なブラウザーにはすべてそれが搭載されています。
- Webに縛られず、サーバーサイドでも動かせます。WebAssembly System Interface(WASI)は、どのプラットフォームにも移植できるように設計されたインターフェース(ABIとAPI)です。Unix系システムの標準インターフェースであるPOSIXに似ており、入出力(I/O)などの機能を提供します。セキュリティーは設計の重要な要素で、サンドボックス化やケイパビリティ指向(ファイルやソケットなどの権限を明示的に要求する必要があります)が含まれます。WASIには、言語間の連携を簡単にする可能性もあります。Dockerの共同創業者であるSolomon Hykesは2019年にこう書いています。「もし2008年にWASM+WASIが存在していたら、Dockerを作る必要はなかっただろう」
どれを選ぶか
- アセンブリ言語を触ったことがないなら、WebAssemblyがおそらく一番始めやすい言語です。とはいえ、機械語にコンパイルされるアセンブリ言語を学びたいなら、MIPSアセンブリを試してみましょう
- x86-64のマシンで作業しているなら(その可能性は高いです)、x86-64アセンブリを試してみましょう
- LISPに慣れているなら、WebAssemblyがS式を使っている点を楽しめるでしょう
- Webアプリを扱っているなら、WebAssemblyが最も理にかなった選択です
- 性能を重視するなら、x86-64とMIPSは優れた選択です。Webの性能を重視するなら、WebAssemblyを試してみましょう