Go の copy はいかにして実装されるか

28531 ワード

Go で slice から別の slice に要素をコピーする際にはたいてい builtin 関数の copy を使用しますが、中身がどのように実装されているかは普段あまり意識しないと思います。そこで中身を深ぼっていったところいろいろと面白い発見があったので共有したいと思います。

本記事では次のことが学べます。長い記事となっていますので、ぜひお時間のある時にお読みいただければなと思います。

  • builtin 関数の読み解き方
  • Go アセンブリの読み書き方
  • Go と Plan 9 の間にある、とある OS の話
  • SIMD 最適化の手法
  • アクセシビリティに配慮した作図

まずはとっかかりとして builtin 関数の実装の起点を探します。少し調べてみると、builtin 関数の使い方は GoDoc があるのでそれを読めば良さそうです。ただ実装自体はこのドキュメントからは明示的に辿れません。実は builtin 関数は、実装がそのままどこかにあるわけではありません。引数に取れる型を柔軟にするために、字句解析、構文解析、抽象構文木(AST)生成までは builtin 関数を1つのシンボル(ノード)のように扱い、AST から SSA(Static Single Assignment form)、延いては Go アセンブリを生成する際に builtin 関数の引数に対応した Go アセンブリを自動生成するようになっています。

ではその自動生成しているコードはどこを読めばよいかというと、builtin 関数の AST におけるノード表現が src/cmd/compile/internal/ir/node.go に定数で宣言されているので、これを元に検索すれば良さそうです。特に builtin 関数の copyir.OCOPY に対応しているので、これ元に検索します。

するとエスケープ解析したり引数の型チェックをしたりしている箇所もありますが、メインの処理を呼んでいるのはここ、もとい workCopy であることが分かります。

workCopy の説明を読むと、Go のコードを

copy(a, b)

としたときに、生成される Go アセンブリは疑似コードで

init {
  n := len(a)
  if n > len(b) { n = len(b) }
  if a.ptr != b.ptr { memmove(a.ptr, b.ptr, n*sizeof(elem(a))) }
}
n;

となっているので、つまるところコピーを行っている実態は runtime.memmove [1]になります。

次に memmove がどこで実装されているか検索する必要がありますが、typecheck.LookupRuntime("memmove") とある通り runtime パッケージにその実態があるか確認しているコードがあるので、目で追う際もコードと同じく runtime パッケージを探せば良さそうです。すると ここ に function body なしで宣言されていることが見て取れます。ご存知の方も多いかと思いますが、Go Spec にも載っているとおり、Go のコードの時点では function body を書かずに、アセンブリなど Go の外側で function body の中身を書くことができます。

A function declaration without type parameters may omit the body. Such a declaration provides the signature for a function implemented outside Go, such as an assembly routine.

function body なしの宣言と function body の中身のつなぎ合わせは、コンパイル後のリンクの過程でお互いのシンボルを照合して行います。

ここまでの処理は go build を行うことで自動的に行われるので、ユーザー視点では特に runtime の中身やコンパイル過程、リンカについて意識する必要はありません。


それではアセンブリで記述される runtime.memmove の中身を深ぼっていきます。

runtime.memmove の実態はアセンブリで記述されていることからも分かるとおり、各アーキテクチャ毎(と一部 OS 毎)に別々に実装されています。

それぞれの実装を見ていっても、それはそれで楽しかったり、新しい発見がありそうですが、本記事では筆者が個人的に一番面白いと思った amd64 の実装を見ていきます。なお Go のバージョンは明記しない限り 1.18 とします。

https://github.com/golang/go/blob/go1.18/src/runtime/memmove_amd64.s

コードを読み始めて一番最初に目につくのは、ヘッダコメントにある Inferno と bitbucket への URL ではないでしょうか。

// Derived from Inferno's libkern/memmove-386.s (adapted for amd64)
// https://bitbucket.org/inferno-os/inferno-os/src/master/libkern/memmove-386.s

実は各アーキテクチャ向けの memmove の実装のほとんどは Inferno と呼ばれる OS のカーネルライブラリに含まれるコードがベースとなっています。

ここで Inferno について少し解説します。1995年ごろに AT&T ベル研究所より開発がスタートした Inferno は、同じくベル研で研究開発が進められていた Plan 9 で培った成果を元に、より幅広いデバイスやネットワーク、アーキテクチャに対応した分散 OS です。Plan 9 の開発には Go の生みの親である Rob Pike が関わっていたのですが、Inferno にも Rob Pike が関わっています。現在では開発自体はほぼストップしているのですが、2005年初めにフリーライセンスのもとOSS化、2021年3月にMITライセンスで再ライセンスされていることから、メンテナンスはされているのかなと思われます。

Inferno のカーネル部には、幅広い環境ごとの差異を吸収して Write once, run anywhere(WORA) でプログラムを動作させるため、Dis と呼ばれる仮想マシンが実装され、Inferno 上で動作するアプリケーションは Dis バイトコードに変換されて実行されます。また、アプリケーション開発用言語として Limbo が開発され、これが Inferno 上でアプリケーション開発を行うための、事実上の標準言語となっています。

Inferno は動作タイプとして主に CPU 上で直接動作する組込タイプと、既存 OS 上で動作する仮想環境タイプの2種類があり、前者のタイプに対応している CPU には x86, ARM, PowerPC, MIPS, SPARC などがあり、後者には Window NT, Windows 95, Unix, Plan 9 などがあります。その移植性の高さから、有志で Nintendo DS で動作させる猛者もいるほどです[2]

こう聞くと、Inferno は OS というより Java に近い仮想マシンなのかなという印象を抱くと思います。それもそのはず1996から1997年にかけてすでにシェアを伸ばし始めていた Java の競合相手して公開されているのです。Java もInferno(Limbo) も WORA でそれぞれ JVM、Dis といった仮想マシン用バイトコードを生成して実行する点においては共通しているのですが、大きな違いとして、JVM はスタックマシン、Dis はレジスタマシンで設計思想が違います。一般的に、スタックマシンはレジスタマシンに比べ実装が容易なのに対し、プロセッサのアーキテクチャに則っていないため JIT コンパイルや複数コアでの並列実行がしにくいという欠点があります(スタックマシンに対するレジスタマシンのメリデメはこの逆です)。ちなみに Android の VM であった Dalvik も Java から変換されたバイトコードを実行しますが、レジスタマシンで実装されています。

閑話休題。

結局のところ Inferno のカーネルライブラリをベースに Go の runtime.memmove が実装された歴史的背景については分からなかったのですが、Inferno の歴史や設計思想から、Go の生みの親である Rob Pike が Inferno に精通していたこと、Inferno が Go と同じくクロスプラットフォームで動作すること、組込で動作するレジスタマシンであったことがベースになった理由なのではないかと考えられます。


さて、Inferno について話が逸れましたが、いよいよ runtime.memmove の処理手順を追っていきます。上記で述べた通り、数ある中で amd64 の実装を見ていきます。細かい注意点ですが、コード自体は amd64 アセンブリで記述されているわけではなく、amd64 アセンブリへ変換しやすい形式で記述された Go アセンブリです。Go アセンブリの読み書きについては本記事の趣旨ではないので以下の2つの記事をご参照ください。