1. 八百万のOSS
  2. #25 go-jsonはなぜ速いのか — ..
#25 go-jsonはなぜ速いのか — Opcode VMとgo:linknameの話 | goccy
2026-10-28 1:22:22

#25 go-jsonはなぜ速いのか — Opcode VMとgo:linknameの話 | goccy

go-jsonは、Go標準の`encoding/json`と互換のインターフェースを持つ高速なJSONライブラリです。今回は作者のgoccyさんをゲストに迎え、主にエンコーダの高速化の仕組みを深掘りしました。


開発のきっかけは、ブラウザゲームのサーバーで大量のJSONを返す際に、エンコードがボトルネックになったことでした。json-iterator/goへの置き換えでも十分な改善が得られず、自ら実装に取り組みます。


go-jsonの中心にあるのは、型ごとにOpcode(命令列)を生成・キャッシュし、VM(バーチャルマシン)で実行する仕組みです。関数呼び出しを繰り返す代わりに、巨大なswitch-caseで命令を処理します。ジャンプテーブルへのコンパイルをアセンブリで確認する方法や、caseの配置によってベンチマーク結果が変動する問題など、低レイヤーの最適化について詳しく聞きました。


さらに、`reflect`のヒープへの強制エスケープを回避するために利用していた`go:linkname`の話へ。標準ライブラリの非公開APIへの依存がGo本体の開発を妨げる事例として、2024年にRuss CoxさんのIssueでgo-jsonが名指しされました。この問題を背景にGo 1.23では`go:linkname`への制限が強化され、Goのソースコードには今もgo-jsonの名前が残っています。


2026年9月のv0.11.0では、コーディングエージェントも活用しながら`go:linkname`を全面的に削除。Go 1.21での`reflect`の改善に加え、`typelinks`を使ったキャッシュをオープンアドレス法のハッシュテーブルに置き換え、読み出しをロックフリーに保ちながらメモリ使用量と性能を改善しました。その結果、bytedance/sonicを上回るベンチマーク結果も得られています。


後半では、さらに高速なvelox-io/jsonの実装にも注目します。Cで記述したVMをELFとして埋め込み、GCCのcomputed gotoで命令を直接ディスパッチする方式です。こうした手法との比較を踏まえ、`encoding/json/v2`への対応やeasyjsonのようなコード生成方式など、go-jsonの今後についても議論しました。


JSONライブラリの内部実装から、ベンチマークとプロファイリングの向き合い方、OSSの保守と設計判断まで。GoでJSONを扱う方や、ISUCONに参加する方にぜひ聞いてほしい回です。


## 関連リンク


- go-json: https://github.com/goccy/go-json

- go-json v0.11.0 リリースノート: https://github.com/goccy/go-json/releases/tag/v0.11.0

- go-json ベンチマークレポート: https://goccy.github.io/go-json/

- go-json Issue #573(velox-io/jsonとのベンチマーク比較): https://github.com/goccy/go-json/issues/573

- golang/go Issue #67401(cmd/link: lock down future uses of linkname): https://github.com/golang/go/issues/67401

- Goのreflect/type.goでgo-jsonに言及している箇所: https://github.com/golang/go/blob/c0be4674865b06f0be851e27cef93136708be506/src/reflect/type.go#L2836-L2846

- Go 1.21 Release Notes: https://go.dev/doc/go1.21

- Go 1.23 Release Notes: https://go.dev/doc/go1.23

- encoding/json/v2: https://pkg.go.dev/encoding/json/v2

- velox-io/json: https://github.com/velox-io/json

- velox-io/json アーキテクチャ解説: https://github.com/velox-io/json/blob/main/docs/arch_2_en.md

- bytedance/sonic: https://github.com/bytedance/sonic

- json-iterator/go: https://github.com/json-iterator/go

- easyjson: https://github.com/mailru/easyjson

- GCC Labels as Values: https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html


─────────────

YouTube: https://youtu.be/kYi8VVimHAE

Web: https://yaoyorozu-oss.henteko07.com/

X: https://x.com/yaoyorozu_oss

感想

まだ感想はありません。最初の1件を書きましょう!

サマリー

Go-json開発者のごっしーさんが、標準ライブラリと互換のまま高速化するJSONライブラリの設計を語る。ブラウザーゲームで大きなJSONレスポンスを頻繁に返し、エンコードがサーバー負荷のボトルネックになったことが開発の出発点だった。Go-jsonでは型ごとにOpcode列を生成してキャッシュし、巨大なswitch-caseのVMで処理することで、関数呼び出しや分岐のコストを減らしている。コンパイラーがswitchをジャンプテーブルにするか、ケースの配置でベンチマークが揺れること、アセンブリで生成結果を確認する方法も説明された。 さらに、reflectによる強制エスケープを避けるためgo:linknameで標準ライブラリの非公開APIを利用していた経緯と、Go側で問題視された背景が語られる。Goの改善によってリフレクションの回避が不要になり、typelinksを使った方式もオープンアドレス法のキャッシュへ置き換え、メモリ使用量と性能を改善した。コーディングエージェントとの対話も実装改善に役立ち、bytedance/sonicを上回るベンチマーク結果につながったという。後半では、C製VMをELFとして埋め込みGCCのcomputed gotoで命令を直接ディスパッチするvelox-io/jsonとの比較、ポータビリティや安全性とのトレードオフ、encoding/json/v2対応とコード生成方式の計画が話題になる。最後に、速度を保つには充実したベンチマークとプロファイリングで結果を継続的に検証することが重要だと強調された。

go-jsonの目的と開発のきっかけ
今回は、go-jsonの開発者のごっしーさんに来ていただきました。 ごっしーさん、よろしくお願いします。
よろしくお願いします。
はい、ありがとうございます。 簡単に自己紹介の方、よろしいですかね。
はい。今は株式会社レイアXというところで、 ソフトウェアエンジニアをやっております。
ごっしーと言います。GitHubのアカウントとかは、 GOCCYというスペルで活動しているんですけど、
毎回読みにくくて、スペルで間違って呼ばれちゃうんですけど、 ごっしーと覚えていただければ嬉しいです。よろしくお願いします。
はい、ありがとうございます。ちなみに間違って呼ばれる場合、 何て呼ばれるんですかね、ごっしーさん以外で。
Cだから、ちーって呼ばれることが多いですね。ごっちーさんとか。
ごっちーさん。確かに。
自分は本名が御島なので、そこから取ってるっていうだけなんですけど。
SじゃなくてCだけど、ごっしーっていう。
ありがとうございます。今回はですね、ごっしーさんに来ていただいて、 GO.JSONの話をしていけたらなと思うんですけど、
一番最初にですね、GO.JSONについて簡単に紹介していただいてもいいですかね。
はい。GO.JSONっていうのは、自分はオープンソースの開発が趣味で、 GOの開発、オープンソースも結構作ってるんですけど、
その中で一番スターが多い、自分の中でも代表するようなソフトウェアになっていて、
作り始めたきっかけが、その前にGO.JSONはどんなものかっていうのを簡単に説明すると、
エンコーディングJSONとコンパチのインターフェースを持ってて、
コンパチなので、インポート分をエンコーディングJSONからGitHub.com スラッシュ ゴッシースラッシュ GO.JSONに差し替えるだけで動くようになるっていうのを目指しています。
差し替えて動くだけだと何が嬉しいのか分からないと思うんですけど、
差し替えるだけでめちゃくちゃ速くなるっていうのが特徴で、
インターフェースは全く一緒なんですけど、裏側でありとあらゆるテクニックを作って早くしているっていうところが特徴かなと思っています。
ありがとうございます。すごい今の説明、簡潔でシンプルでめちゃくちゃいいなと思ったんですけど、
インポート分を差し替えて、その差し替えただけで動くし、なおかつめちゃくちゃ速くなるっていうようなことなんで、
すごいパンチライン強いですね。
ありがとうございます。
オープンソース作るときにいろいろどういうコンセプトで作るかっていうのを考えて作るんですけど、
このJSONに関しては、JSONって本当にプリミティムなもので、どなたでもいろんな方法で使うと思うんですけど、
いろいろあるライブラリの中で自分が作る面白いものを作りたいってなったときに、
標準ライブラリと同じインターフェースなのに、もっと早く使えるとか、高速な動作を期待できるみたいなのは、
フックとして面白いかなと思って、速度をコンセプトに作ったライブラリです。
カリカリに内部実装ではチューニングして、速度を目指して、ベンチマークで速度を測りつつ。
普段はやらないような、Goでいうとアンセーフパッケージとかを使って、
一見怖いような処理っていうのも、自分のソフトウェアのコンピューターサイエンスの知識とかを上手く使って、
アンセーフな操作を安全に使うことに注意しつつも、使うっていうことに物応じせずにガンガン使って、
高速化していくっていうのが、このライブラリのコンセプトですかね。
ありがとうございます。その内部実装、これからできる限り深掘って話していければなと思うんですけど、
ちょっと先にGo.JSONが依存されているライブラリだったりとかツールとかって、何かご存知のものあったりします?
依存されている。そうですね。これは結構いっぱいあるんですけど、例えばGitHubのページを見ると、
Used Byっていうのがあって、そこに230系って書いてあるんで、とんでもない量利用されてるってことになっていて、
これはたぶんGoのサードパーティーのオープンソースだと、たぶん世界2位ぐらいのJSONのライブラリですけど、
たぶん1位がJSON Iterator Goだと思うんですけど、
JSON界隈であればGo.JSONは2番目ぐらいにめちゃくちゃ豪華に使われているようなライブラリになっていると。
すごい。
具体的に何が何に使われているかっていうのは、前に調べたことはあるんですけど、
今最新のやつを直近で見てなかったので、わかんないですけど、気になる方は見てみてください。
いろんなフレームワークとか、あとKubernetesでもたぶん感染症の異常に入ったりとかすると思います。
Kubernetesでちょいちょい機能になっているの見ますね、Go.JSON。
Kubernetesで使われてたら、デファクトスタンダードなんじゃないかなと思っちゃうぐらいデカさのものなんですけど、
今回はそのぐらい巨大なライブラリというかめちゃくちゃ使われているGo.JSONの中身について聞いていけたらなと思うんですけど、
ちょっとさっきも話の中、紹介の中であったんですけど、速度が一番差別化要因っていうところかなとは思うんですけど、
Mazmoってそもそもエンコーディングゼーション、標準パッケージのエンコーディングゼーションが遅いなと思ったきっかけとかってあったりするんですか?
そうですね。自分は2019年ぐらいに、20年からメルカリという会社のお世話になったんですけど、その前の会社がゲーム系のスタートアップで、
ブラウザーのゲームを作ってたんですね。JSONでサーバーから頻繁にゲーム系のレスポンスを返すわけなんですけど、
ブラウザーなので基本的にキャッシュとかもそんなに使用できないし、常に結構でかいレスポンスをJSONで返しまくってたんですよ。
そういう都合で、JSONのエンコーディングが結構ボトルネックになって、サーバーに負荷がかかってることに気づいて、
当時デファクトスタンダードだったJSON Iterator 5っていうのが、エンコーディングJSONとコンパチのインターフェースで早くするっていうコンセプトで、
じゃあこれに置き換えてみたらいいんじゃないかと思ってやってみたんですけど、多少早くなったんですけど、そんなに期待することでもなくて、
うーんって思ってその中身を調べてみたら、なんか自分の方がもうちょっと早く実装できるんじゃないかって思ってしまって、
そこからこうなんか作ってみたくなっちゃって、作り始めたっていうのがきっかけなんですけど。
なるほど、元々業務でめちゃくちゃ巨大なJSONを高頻度で送りつけてくるクライアントがいるような環境で、サーバーの負荷が問題だったと、
エンコーディングして何らかの処理をするところが問題で、そこのスループと上げたかったっていうのが一番最初の課題点として。
そうですね、はい。
で、その後、他のツールとかを見て、他のライブラリとか試してみたけど、微妙ってなって、自分で中身見てみたら、
いや、これは自分だったらいけるってなったんで、GoJSONを作ったっていうのが一番最初のモチベーションなんですね。
そうですね、そうですね。
当時くらい、多分ほんと当時同じくらいだと思うんですけど、自分もGoのJSONちょっと遅いなって気になって、
なんか当時PHPとも比較したんですけど、PHPでも結構負けることあるんですよね、GoってJSONの遅さで。
なので、正直他の言語と比べても、正直GoはJSONそんな早くないなって思ってましたね。
なるほど。
なんかそこのきっかけというか、なんでそれが起きているのか、PHPの方がちょっとだけ早くなってしまっているのかみたいないうところも、
今回触れられるかどうかわからないですけど。
実装全然違うんで、触れないと思いますけど。
そのGoJSONの中でどうやって早くしていけるのかみたいないうところを話していければなと、ちょっとずつ入っていければなと思うんですけど、
まず最初に速度改善した、コンパチブルでインポート文を書き換えれば既に使えるようにしたっていうのはわかりやすいかなと思うんですけど、
そこから一番最初のステップって何だったんですかね。一番最初の切り口というか、高速化の切り口。
なるほどです。めちゃくちゃいい質問で。
Opcode VMでエンコーダを高速化する仕組み
そもそもエンコーティングJSONの実装とか、JSON Iterator Goの実装を読んで、自分の方が早く作れるんじゃないかと思ったきっかけが、
自分は羊が言語処理系の研究室から出てるんですけど、研究室で作ってたプログラミング言語を、バーチャルマシンとかJITコンパイラーとかを積んだ言語を作っていて、
バーチャルマシンの説明とかもした方がいいですかね。
そうですね、VMについて。
言語処理系って、普通評価するときにASTっていうのを作って、それをなぞりながら評価していくっていう方式と、
それをもうちょっとキーをたどっていくのって、ポインターを行ったり来たりしながら評価していくんで、結構遅いんですよ。
それをもうちょっとデータ構造とかも改善して評価できないかっていう方式で、バーチャルマシン方式っていうのがあって、
CPUが命令列を評価しているのと同様に、CPUの実行系をソフトウェアでエミュレーションするようなイメージですね。
なので、レジスターとか、CPUのプログラムカウンターとか、オペレーションコード、オペコードとかオッコードとか呼んだりするんですけど、
そういう命令列みたいなものを配列の中に命令ストラクチャーみたいなのを並べていって、それを上から順番に評価していくと。
巨大なスイッチケースがあって、それをフォア分が囲っているような構造が基本的なバーチャルマシンの構造なんですけど、
オペレーションコードの配列みたいなものを想像してもらうと、0番目から順番にオペレーションコードを評価していって、
スイッチケースのどのケースに入るかというのがオペレーションのタイプみたいなのがコードの中に入っているので、
それでケースに入っていって、何かしら処理したらループの先頭に戻ってっていうのをずっと繰り返しながら評価していくと。
こっちの方がエヴァルでASTを辿っていくよりも高速に評価できるということで、
基本的には現代のプログラミング言語というのはVMのバーチャルマシン型の実装というのを採用していますと。
よく聞くJITコンパイラ、Just-In-Timeコンパイラというのはこのバーチャルマシンをさらに進めたもので、
オペレーションコードを評価するときに巨大なスイッチケースのケース分に入って評価して、
またループの先頭に戻ってまたスイッチのどのケースに入るかというのを評価して入ってっていうので、
分岐ミスみたいなものがあるわけですよ。ケース分に入るっていう時にイフ分が入るわけじゃないですか。
どのケースにジャンプするかみたいな。これをどうやって判定処理みたいなのをなくせるかっていうのが重要で、
ホットパスにおいてはこのオペレーションコードっていうのを順番に高評価してほしいっていうのがあるんだったら、
それを評価する側のケース分、それぞれのケース分をいってみればコピペして、
オペレーションコードの順番通りのケース分を上から順番にいって貼り付けたものが用意できれば、
フォア分の先頭に戻ってジャンプするっていうのはなくなるじゃないですか。
だからそれを切って貼ってみたいなものを一つのメモリ上にアラインさせて、
上から順番に直列に実行していくみたいなのがJITコンパイラーとかのよくやってるアイディアだったりするんですけど、
そういう研究とかずっとやってて、VMが早いっていうのを知ってたので、
JSONに関してもエンコーディングするときって、
コーのストラクトがあって型が決まっていて、最初のフィールドはイントですと、次のフィールドはストリングですってなったときに、
このフィールドをイント型としてエンコーディングして、次のフィールドをストリング型として値をエンコーディングしてっていうのを繰り返していく処理って、
これはVMと一緒だと思うので、VM型が高速なのを知っていた自分にとってみたら、そういうアプローチを取れば早くなるだろうし、
最終的にはJITコンパイルとかも視野に入れれば、よりカリカリにチューニングしたエンコーダーがくれるんじゃないかな、みたいなところは出発点で実装し始めましたね。
なるほど、今ちょっとすごく雑に要約すると、まずVMって言ってたマーチャルマシンの話は別にOSとかは関係なく、そういう考え方というか、
そういう処理方法があるよというようなお話ですよね、VMって言っている話とか。
いわゆるウェブの開発者がよく使っているマーチャルマシンとは若干違うというような単語ですね。
そうですね、何を仮想化しているかというのでコンテキストが違うと思うんですけど、よく言語処理系とかで言うマーチャルマシンはそういうアプローチかなという。
既存のGoJSON以外のJSONのエンコーダーだったりは、基本的にASTによって機構造でJSONの構造を解析していくというような処理がされていたけど、
5CさんはVMの知識というか研究されていたので、VMが速いっていうことが知っていたから、これはJSONにも適用できるんじゃないかというところでやってみたというのが、
GoJSONの核になるところみたいなところですかね。
それで言うとちょっと自分の説明があれで、もともとにエンコーディングJSONとかがASTを同行するっていうアプローチではないんですよね。
なるほど。
そもそもプログラミング言語だったらPaaSっていう処理が必要で、テキストで書かれたものを機械的に解釈するためにPaaSでASTにするっていうフェーズがあるから、
そのASTを評価するって話になってくるんですけど、JSONの場合はテキストを評価する、特にエンコーディングの場合はテキストを評価するわけじゃないので、
もともとGoのストラクトとかにカターが決まっているものにマッピングされたものをどうやって高速にエンコーディングしていくかって話で、
アプローチとしては例えば1つ目のフィールドがイント、2つ目のフィールドがストリングってなったときに、
イント用の処理関数とストリング用のエンコーディング関数みたいなのを用意して、
それをファンクションで、さっきのオッコードの話じゃないですけど、
フィールドの最初の評価はイントの関数を差し込んで、次のストリングの評価のときはストリング用の関数を差し込んでっていう、
処理用の関数を直列にドライバーの関数を差し込んだ配列みたいなのを用意したとして、
その配列を先頭から順番に評価して、関数を呼び出して、次のパンチに移動して関数を呼び出してっていうのは処理とかが考えられると思いますと。
エンコーディングジェイソンとかもそれぐらいのことは確かやってたと思うんですよね。
なんですけど、関数を、次の話に行っちゃうんですけど、関数を次に進んで呼び出すみたいなものよりも、
スイッチケースでケース部に飛んで処理するっていう方が関数の呼び出しオーバーヘッドがなかったりとか、
いろんなメリットがあってそっちを採用したんですけど、
そういうVMでスイッチケースでやってたみたいなのが頭にあったので、
関数ベースの呼び出しよりも早くできるんじゃないかみたいなのがあったというのが大きいですね。
理解しました。関数呼び出しのオーバーヘッドがでかいから、関数呼び出しをしないVMでの処理の方が早いんじゃないかというようなところ。
そうですね。
仮説として、それがあったと。
ゴージェイソンのそのコアの部分、その速度、この高速化の速度を支える部分ではこのVM、オプコードって呼んでるんですかね。
はい。
っていうところがある。オプコードVMがこの高速化を支えてるみたいな理解であってますかね。
あってます。
じゃあここがもう心臓部なんですね。このVMの処理の部分が。
そうですね。やはりどれだけ関数コールを避けるかっていうのが大事になってきて。
結局そうですね。難しいんですけど、エンコーダーの話で言うと本当に球状一番早い処理っていうのは文字列をそのまま書くことなんですよ。
例えば、中括弧があって、foo1みたいな、JSONのフィールドがあって、とじ括弧みたいな。
これをそのまま文字列にして書き出すのは、それは一番早いじゃないですか。
単純に入力を出力するってことですよね。
そうです。文字列がコードに焼き込まれてて、それをそのまま書き出したら当たり前の話なんですけど早いと。
でも、fooっていうフィールドまでは固定ですけど、値として1を出すのか2を出すのかっていうのは動的に評価しないといけなくて、
逆を言えばそれ以外のところっていうのは固定の処理に近づけられれば近づけるほど早くなるわけですね。
あとその1とか2とかのところだけどうやって高速に計算して、文字列、結合ではないんですけど文字列として一緒に書き出すかみたいなところが、
エンコーダーの一番基本的な考え方になるんですけど、
それを一番最速だとしたときに、あとはそれぞれのジェンソンの値に関するものをどうやって文字列にしていくかっていうところを、
より少ない命令数でどうやって実現するか、条件分岐をいく、どれだけ減らせるかとか、
関数を呼び出すときっていろんな処理が走るので、
例えば引数に渡すものとかスタックに似合ったりとか呼び出す、ただ関数のアドレスにジャンプしておしまいっていうわけじゃなくて、
引数にあたる値を対比させたり戻したりとかっていうスタックの操作が必要だったりとか、
結構コストがでかい処理なので、そういうのをいくら減らせるかとか、そういうのを極限まで決めて考えた結果、
このスイッチケースの処理に行き着いたという感じでしょうか。
なるほど、VMの中にできる限り関数を呼び、他の部分の関数を呼び出さないようにVMの中にできる限り処理を押し込んでいく、
みたいなアプローチ方法を今取ってる。
そうですね。
すごく雑に言ってしまうと。
そうですね。
その中で他のライブラリとかでこういったVMの実装とかって、JSONに限らずで構わないんですけど、
ご主人さんは知ってたりしました、その当時。
他にやられてるライブラリとかがあるかどうかとか。
型キャッシュとジャンプテーブルの最適化
そうですね。結構自分はサードパーティーのJSONライブラリを調査したんですけど、
同じようなアプローチを取ってるライブラリなかったですね。
なかったっていうのもあって、実装してみようかなとも思ったんですけど、
大抵がやっぱり、ちょっとまた技術の話に戻ってくるんですけど、
Goの型って、例えばリフレクションとかでreflect.typeっていうので、
メタ的に表現できるんですけど、
例えばintとかstringみたいなのをreflectのtypeofとかtypeforとかで、
リフレクション用の型に写像することができて、
その型、要はintとかをstringとかをメタ的に総称するリフレクションのタイプっていうもののアドレスって、
バイナリの中で固定になるんですよ。
こういう性質があって。
それはバイナリの中でっていうのは、実行中の中ですかね。
そうですね。つまりリフレクトのタイプをintとかstringとか、
自分で作ったストラクトから手に入れた時に、
そのリフレクトタイプ型の値のアドレスって常に一緒なんですよね。
これはGoの実装というか、内部の実装している人は知ってるんですけど、
固定になるっていう性質があって、
バイナリの中でタイプが格納されてるテクノンっていうのはあるんですけど、
そこのアドレスを足してくれてるんですよ。
なので、つまりこのタイプをどうにかしたいっていうふうなことを考えた時に、
このタイプに対して何かアクションを起こしたいって思った時に、
そのタイプのアドレスを、自分は便宜的にタイプIDって呼ぶんですけど、
そのタイプIDをマップのキーにしてしまって、
そのやりたいアクションっていうのをバリューにしちゃえば、
基本的には型による何かっていうのを全部ハッシュして扱うことができるようになるんですよね。
なので、例えばJSONにおける処理っていうのも、
同じ型に対してエンコーディングしたいっていうのがほとんどのケースなんで、
1回目に何か重い処理をして、
例えばさっき言った自分だったらVM方式で評価したいから、
この型に対するオッコードを生成して、
VMで評価する前準備をしますみたいなコンパイル処理を、
それなりに重い処理なんですけど、
これをタイプIDをキーにマップの中に格納しておけるので、
2回目呼び出した時は、
常にキャッシュされたオッコードをタイプIDから引っ張っていって評価すればいいだけになるみたいな。
この型から任意の処理を引くっていう処理が肝になるんですけど、
これは割と3rdpartylibrary.comもやってましたと。
何ならエンコーディングJSONもやってる処理で、
ただその型からキャッシュした処理、
型でキャッシュした処理自体がどこまでチューニングされているかっていうので、
速度に差が出てきているっていうのが実態だというのが。
あれですね、1回処理する前にJSONの入力のJSONを中間コードみたいな形で変換してるんですね、一旦。
そうです。
それをVMに対して渡して処理をさせるみたいなことが現状されていると、VMの中で。
そうですね。
その中のバリューの、バリューを処理する関数を呼び出すのをできる限り少なくするチューニングをされているっていうところが、
差別化の部分っていうことなんですね。
理解できます。
やっぱりこのOPCODE VMっていうところが一番の心臓部分っていうところなんですけど、
そこまでのカリカリなチューニングまでは他のライブラリーもやられてなかったっていうようなところなんですけど、
逆にここまで聞いている限り、このVMというものが銀の弾丸みたいな感じで、
何でも適応すれば速くなるんじゃないかなみたいな感じで聞こえちゃったんですけど、逆に遅くなることってあるんですか?
遅くなることは難しい質問ですね。
やってみて逆に遅くなっちゃったとか、そういう失敗なんとかあるかなと。
そうですね。実際やってみて遅くなったは実はないんですけど、
でもコンパイルしたときの命令、巨大なスイッチケースのケース分の順序によって実行時間がちょっと変わったりみたいな、
どの環境でどういうふうにバイナリにするかによって、コンパイラの挙動によってケースが前の方にあるのか後ろの方にあるのかみたいなので、
ヒット率が変わるみたいな、ソフトウェアエンジニアでは調整できないような領域というか、
そういうのでベンチマークのパフォーマンスが結構変わったりするのには苦しんだことあるんですけど、
でも長期間実行していくと基本的には鳴らされて、そういうブレ、揺れっていうんですかね、ケース分がどこにあるのかみたいなので、
ヒット率が変わってどうこうみたいなのが基本的には積極化されてあんまり気にしなくていいようになっていくんで、
そういう観点で言うとあんまり気にしなくていいんですけど、ベンチマークするときには困ったとかはあったのと、
あとは理論上ケース分を無限に増やしていけるんだったら、いくらでも早いことができますと、
例えばイントとストリングっていう順番で来るフィールドが並ぶっていうのに対応してイントストリングという命令を作っちゃいましょうみたいな、
そういうのをイントストリングイントだったらイントストリングイントっていう命令作っちゃいましょうみたいなので、
事前に数百どころか数万とか数十万の命令を用意して、
5のファイルにしておいてコンパイルすれば、当然命令としては一つの命令になるんで、
その分岐がなくて早くなるんですけど、ファイルがとんでもなくでかくなると、そもそもコンパイルできなくなっちゃいますよねっていう話とかありますよね。
現実的な落としどころはあるが、最適化の現実的な落としどころはあるが、まあでも理論上は突き詰めればどこまででも行ける高速化手法ってことなんですか。
コンパイラーがスイッチケースをジャンプテーブルにしてくれるかどうかっていうのが重要で、
一番まずいのがケースを一番上から順番にif分で評価していくっていうパターンで、
それだとケースが後ろの方にあればあるほど遅くなってしまう。
そうじゃなくて、O1でジャンプテーブルでどのケースどこに配置しても同じ計算量で行ってくれるっていうのが前提として必要なんですけど、
Goのコンパイラーの場合ちょっと忘れちゃったんですけど、条件を満たすとジャンプテーブルにしてくれるっていうのがあって、
それは満たせてるから大丈夫っていうことになってますね。
それは増えても大丈夫だったかな。
じゃあGoJSONの弱点と言っていいのかわかんないんですけど、
Go内部の実装がそこの部分if分に切り替わるみたいな変更があったら途端に破綻するっていうことなんですね。このやり方においては。
そうですね。そういうコンパイラーの変更、仮に将来あれば。
でもどうなんですかね。パフォーマンスがまずくなるのでないんじゃないかなと思いつつ、
なんか1個気になったのがさっき言ってた話ってC++のテンプレートとかだともしかしてできたりする話なんですかね。
C++のテンプレートだと、
コンパイル時に色々生成するじゃないですか。
アプローチとしてはあると思います。
Goだとないアプローチですけど。
そうですね。全然ある気がしますね。
ジャンプテーブルの確認で、Goってコンパイルオプションが出せば、アセンブルは直接出せるじゃないですか。
はい。
そういうので確認したりするんですかね。ジャンプテーブルになってるかどうかって。
そうですね。実際にはそうやって確認してます。
なるほど。ありがとうございます。
そのジャンプテーブルの確認自体はCIで回してるみたいなことではないって感じですか。
そうですね。ただもし変わったらとんでもなく遅くなるので、
一応CIでマスタートよりも劣化してるかどうかっていうのはプリリクエストでベンチを回してるので、
プリリクエストの時点とかでは気づけますかね。
ただいきなりそのGoのバージョンが変わって、なんでも考えにくいですけど。
Goの人たちってそういうのすごい気にするんですよね。
バージョンアップして特定のライブラリーの速度がめっちゃ落ちましたとかって言うとすごい気にする印象ありますね。
そうですね。
あと基本的にはジャンプテーブルにしたほうがいいんで、
そういうふうにコンパイラ側を作っていくようになるんじゃないかなとは思ってるんですけど。
ありがとうございます。
ほかにこのVMの話してきたかな。そこがコア部分なのであれなんですけど、
VMの話してきたかなと思うんですけど、
他に高速化をしたアプローチ方法ってあったりしますか。
そうですね。
この作った当時の高速化で言うと、
エンコーダーはでも多分それが肝で、
あとは、
go:linknameとGo標準ライブラリの設計問題
Goにリンクネイルっていうディレクティブがあって、
それを駆使することによって高速化したアプローチはいくつかありますと。
当時作り始めたのが2020年とかの時代なんですけど、
この時はそのリフレクションのさっき言ったリフレクトタイプとかリフレクトバリューっていうのを作るときに、
必ずアロケーションが発生したんですよ。
Goの話で言うと、
ヒープに逃がす、エスケープするっていうふうに表現するんですけど、
見た目上なんかそのスタックで処理できるように見えたとしても、
そのリフレクションにするときに、
Goの内部の事情で、リフレクションの一数に渡された値を、
そのヒープに逃がすような処理が入ってるんですよね。
分かりやすいところで言うと、リフレクトバリューオブみたいなので、
intの値を渡しますと。
そしたら、確かその値を、
当時の想像もちょっと違ってたら申し訳ないんですけど、
グローバル変数に出して逃がすか何かって、
強制的にエスケープされるような処理が入ってた記憶があって、
int型だったらそのまんま引数を関数の中で処理してあげれば、
その引数自体エスケープされないはずなんですけど、
それを強制的にエスケープするようなコードが中に入っていて、
それは値に対するリフレクトバリューって、
同じものをメタ的に表現したもので、
元になってる値の方がフリーされちゃったりすると困るっていう発想からだったと思うんですけど、
当時ちょっとやりすぎな面があって、
リフレクトタイプとかバリューを作るときに、
どうやってエスケープさせないで値を作れるかみたいなことを考えて、
Goのリンクネームっていうのを使うと、
標準ライブラリとかのプライベートのAPIもサードパーティーのライブラリから直接呼び出せるんですけど、
それを使ってリフレクションが中でやってる処理を自分で型変わりしてやることによって、
リフレクトライブの公開APIを通すと必ずエスケープされるようなものを迂回して、
エスケープされずにリフレクション相当の処理をやるっていうことをやるためにリンクネームを使ったりか、
あとはさっき言った型から対象の処理を引くっていうやつって、
素朴に実装するとグローバルにマップ型でキーがリフレクトタイプ型とかになって、
そのバリューにファンクションみたいなのを想像すると思うんですけど、
そうするとマップなので、当然実行中に新しくキャッシュが生まれて、
マップへの書き込みとかもあるから、グローバルですし、ロックしたりとかしないといけませんと。
ってなると、結構マップだとしても引くコストっていうのが高くつくなと思って、
これをスライスにできないかっていうのを考えて、
型を格納したアレイを用意して、直接インデックスを計算して、
そこにアクセスしていくみたいなので、ロックフリーにできないかっていうのを考えて実装したんですけど、
それにもリンクネームを使ってて、
リフレクトライブラリの中で、タイプリンクスっていう内部APIを使ってるんですけど、
これはさっき言った型のアドレスっていうのはバイナリの中でアラインされてずらっと配置されてるんですけど、
それをタイプリンクスってAPIが型のアドレスを順番に返してくれるAPIなんですね。
なので、それで型を網羅しつつ、使ってるアドレスみたいなのが全部わかるんで、
それをうまくスライスに適用してあげれば、
あらかじめJSONライブラリに渡ってくる型っていうのも、
事前に把握してるアドレスのどれかでしかないので、
インデックスが計算できて、そのままアクセスできるじゃんみたいな。
そっちの方が早いでしょうっていうので、実装してやってたりしました。
リンクネームっていうのが、Goの中のライブAPIを強制的に呼び出すような操作ってことかなとは思うんですけど、
それを使って内部実装の内部の非公開APIを呼び出すことで、できる限り高速化をしていった。
やつが今言ってくれたリフレクトタイプとタイプリンクスというところなんですかね。
このリンクネームを使って内部API呼ぶっていうこと以外で、
ちなみに回避策は当時なかった感じなんですかね、調べた感じでは。
回避策っておっしゃってるのはどういう
リフレクトタイプとかでアロケーションが起きてしまったりとか。
ないですね。リフレクトタイプを作るためにはリフレクトのその公開APIを基本的には使う必要があって、
それ使うと必ずどんな手段を取ってもエスケープされちゃうっていうのが当時の2020年当時の挙動でしたね。
なので、例えばタックだけで処理できるようなエンコード操作みたいなのでも、
必ずヒープに逃げちゃうからアロケーションとしては1回以上アロケーションされちゃうみたいな。
なるほど、じゃあもう本当にリンクネーム使って内部API呼ぶしかなかったってことですよね、2020年の当時くらいは。
そうですね、それを採用するかどうかは別なんですけど、自分はそういうことをしてまでも1命令でも削りたかったみたいな。
アロケーションを1個でも多く削って、アロケーション削ると、そもそも当時ゼロアロケーションライブラリーとかも結構流行ってると思うんですけど、
ログとかロガーとかもよくあると思うんですけど、アロケーションってその数が1回でもあったら結局長い目で見るとGCに負荷として乗っかってきて、
GCが一番パフォーマンス上はボトルネックになりがちなので、GCをどうやって発生させないかっていう観点も大事なんですけど、
そこに効いてきちゃうので、ゼロに近づければ近づけるほど良いというのも大事かなと思って、こういうチューニングもしてます。
でも本当にカリカリにチューニングすると決めた結果、リンクネームを使ったっていうようなところかなと思うんですけど、
このリンクネームを使って何か不都合とかあったりしました?
不都合。そうですね。
実装上の不都合とかはなかったですけど、2024年、4年ぐらい経った後ぐらいに、
GOの地域学国さんからゴージェイソンがリンクネームのことで名乗らされるっていう話があって、
日本でもいろんなところで反り上げられてくださったりしてる方ももしかしたらいらっしゃるかもしれないんですけど、
そのGOの一周でも5Cゴージェイソンっていうのがくばね鉄とかで使われてるライブラリとしてあるんだけど、
このライブラリがリンクネームっていう黒魔術でGOの標準ライブラリのプライベートAPIを利用してるから、
自分たちがプライベートAPIを気軽に変えられないと困ってるみたいな話を一周に書いてて、
だからこういうのがGOジェイソン以外にもいくつか有名なライブラリであって困ってるから、
今後この標準ライブラリのプライベートAPIを許可されたサードパーティー以外は絶対に使わせないみたいな機能が入るっていう経緯につながっちゃったみたいなのがありましたね。
なるほど。GOの言語の作者から名指しでGOジェイソンが内部API使ってるぞと。
こいつらのせいで内部APIの使用変更できないので、使わないでくれっていうのを名指しで言われてしまったっていうところですね。
そうですね。そんな感じのニュアンスですね。
結構話題になりましたね。GO界隈だと。
なんかラスコックスに名指しされたみたいな感じですね。
自分のほかだとはじめほしさんとか話し方の対象だったと思うけど、個人でこういうふうに個人が開発してるもので名が上がるっていうのはほぼなかったんじゃないかなと思うんで、なかなか衝撃的な話でしたね。
いいというか面白い面もあるんですよね。結局これって自分が趣味で、こういう早いものがあったら面白いかなと思って作り始めたもので、それがワールドワイドで使われるようになって、だからこそ困ったみたいなのを本
家から言われるみたいな。
別に自分としてはできる範囲でやってるだけであって、リンクネームっていう機能を提供したのはあなたたちでしょって思うんですよね。
あとちょっと愚人っぽくなるんですけど、Goのエンコーディングジェイソンライブラリって、安心・安全を売りにしてて、利用者が安心して使えるライブラリ。
速度は重要なら理解してるけど、それよりも壊れないこととか、それによって脆弱にならないこととかを重視してる。その思想自体はすごく理解できますし、それが標準ライブラリになっていることのありがたみっていう
のもあって、
だからこそサードパーティーで安心を使って対象的なものを作っていくっていうモチベーションにもつながってはいるんですけど、エンコーディングジェイソンってリフレクトを使ってるんですね。
リフレクトを自分の説明にも言ってましたけど、型でキャッシュして処理呼び出すみたいなのをやってるんですよ。
リフレクトライブラリってUnsafeに依存してるんですよ。
めっちゃ使ってますね。
そうなんで、間接的にUnsafeパッケージに依存してるじゃんとは思うんですよ。
直接エンコーディングジェイソンからUnsafeは買ってないんですけど、それっていいのかなって思いますし、
このリンクネームの話もサードパーティーに禁止するだけじゃなくて、自分たちの標準ライブラリの開発にも全部禁止するんだったら筋が通ってるなと思うんですけど、
標準ライブラリの中だけではこのリンクネームガンガン使って、お互いに標準ライブラリ内のパッケージに移動しまってやってるんですよ。
それってパッケージのデザインとしていいのかみたいなのがめちゃくちゃ思うところがあって、なんかこう、うーんとは思うんですけど。
なるほど。
そもそもこの名指しされたっていうところはゴージェイソンがそれだけの影響力が得られたっていうところですもんね。
それ自体はかなり嬉しいことですよね、ご試算的にも。
そうですね。実際に書いて、こうやって書かれて話題にしていただいてっていうのは、一切そういう気持ちでいいのかは別として、
悪いというか、申し訳ないとか、やっちまったみたいな気持ちよりかは、どっちかというと取り抜けてみる。
嬉しい気持ちの方が多いキーですよね。
そもそも趣味で作ってるもんだから、それがここまででかい話題になれたこと自体が良かったかなとは思いますけど。
ゴーのチームの人ですけど、ラスコックスから名指しされるって本当にないし、ゴシーゴージェイソンに関してはゴーのソースコード内に例外として書かれてるんですよ。
普通にソースコード内に、これは例外ですみたいな感じで。そこまで行くライブラリーってマジでないんで、これは名誉だと自分は思いますね。
ありがとうございます。
そのゴーの仕様に対しても影響力を与えたライブラリーということですよね。
確かにそういう見方ができますからね。
素晴らしいです。それ自体に関してはもう影響力素晴らしいですよね。
それとは別で、標準パッケージだけパッケージの思想違くないっていうのは、それ自体今聞いてて確かにそうだよなってちょっと思っちゃいましたね。
標準パッケージだけあれですね、お互いがお互いに内部APIを依存し合って破壊できないようにしてるんですもんね。
リンクネームってすごく便利で、普通ゴーってインポート文をお互いにAとBというパッケージがあって、お互いのインポート文を書くと循環参照になってコンパイルできないんですけど、
このリンクネームってリンクって言ってるだけにあって、リンカーのタイミングでシンボル解決してくれるからコンパイルのときは別にリンクしたいエイリアスみたいなもんなんですけど、
その対象のものが存在してるかどうかっていうのを気にしてなくていいんですよ、リンクのときまで。
だから最終的にリンクするときにリンク対象のどこかにシンボルがあればリンクできるからそれ使えるっていう話になってて、遅延できるわけですよね、解決のタイミングを。
なのでさっき言ったインポート文の循環参照みたいなものをリンクネームで避けることができるんですよ。
AっていうパッケージをBから書いたいときに、そのBの中にAへのインポート文だけじゃなくて、リンクネームでAへのこのシンボルを利用するみたいなものを書くと、
それはリンク時に解決されるから、そのAへの依存っていうのをコンパイル時に作らなくてよくて、でコンパイルできるみたいな。
これをこの性質を使って標準ライブラリの中でも、循環参照っていうのは気にせずにお互いのライブラリのAPIとかを利用し合うみたいなことができるんですけど、
それもいいのかっていう、その循環参照っていうデザインに働いたことを自分たちで標準でやっていいのかみたいな話があって、
自分は一貫性を持ってほしいなとは思うけど、そういう便利テクニックがありますという紹介です。
リンクネームの機能自体めちゃくちゃ便利で、いろんな使い方がきっとあるんですよね。
その遅延、リンクする時まで解決しないという遅延実行ができるからこそいろいろできることはあるんだけど、標準パッケージの方で乱用されててどうなんだっていうところで。
ちなみにこの標準パッケージがリンクネームめちゃくちゃ使っていてみたいな話って現状でもあんまり変わってないっていう理解であってます?
使ってると思います。どこで何使ってるかみたいなのを正確にバーッとは言えないんですけど、
使ってる例は知ってるので、現代の最新のコンパイラーでも多分やってはいますね。
じゃあその方針自体は特に変わってなさそうっていうところ。
linkname削除とオープンアドレス法への移行
そんな感じでリンクネームで名指しされたっていうすごく名誉なことかなとは思うんですけど、これを最近解消したって聞いたんですけど。
そうですね。これの話は2024年とかで、今年6年、26年で2年ぐらいどうしようかと思ってたんですけど、コーディングエージェントが出てきてくれて、
背中を押してくれたので、この前のシルバーウィークのときにがっつりJSONの改善に取り組んで、このリンクネームを全部削除して晴れて。
もう何の後ろ、メタリングの原則。しかもリンクネームは削除しただけじゃなくて、より速度の面でも改善できて、
ずっとバイトダンスソニックっていうバイトダンス社が出してるソニックっていうJSONライブラリに速度の面で勝てなくて、
それが快速っていうところからは一歩遠いくなってしまっていたみたいな、ライブラリ的な存在だと勝手に思ってたんですけど。
それにもベンチマークで結構勝てるようになって、ソニックが公開してるベンチマークでもほぼほぼ8割9割型5JSONの方が速くなっているっていうような状況で、
これをAIの力と、この2年ぐらいこうやったら速くできるんじゃないかっていうのを温めてたアイディアというのを実現して達成したものになります。
すごいですね、2年前はリンクネームを使わないとちょっと難しそうだったのが、この2年経ったらリンクネーム使わなくてもいいし、プラスアルファ早くできてしまったっていうところなんですね。
そうですね。
具体的にどういったことをやったかって聞けます?
そうですね。リンクネームを使うきっかけだったモチベーションが、まずリフレクトのタイプとかバリューをゼロアロケーションで使いたいっていうモチベーションだったんですけど、
これに関しては、2024年ぐらいの時点で、もうGoの標準ライブラリー側に強制エスケープしないような実装が入って、あるべき姿になったというか。
それで、このリンクネームを使ってリフレクションを迂回するというか、アプローチ自体がいらなくなったっていうのが一つ大きくて。
タイプリンクスに関しては、もちろんスライスの方が早いんですけど、スライスを作るときに、バイナリーにある型を全部なめて、
その型の数によってスライスの容量が可変して大きくなっちゃうと。
最大は10メガ以上のメモリをそれで食ってたりとかして、メモリのフットプリントが良くなかったっていうのが課題感としても自分の中であって。
なのでリンクネームを残してあえて固執して使ってやるメリットがそこまでないかもと思い始めて、新しいアプローチに切り替えたんですね。
それがオープンアドレス方式で型のキャッシュっていうのを作って引くっていう方式にアップデートしたんですけど、
これやるとオープンアドレスなんで、最初に型のキャッシュを引くために型のアドレスをハッシュ化して、
オープンアドレスのどこのバケットにアクセスしに行くかっていうインデックスを決めて、そこにアクセスしに行くわけなんですけど、
大抵そこのアクセスしに行った先でヒットするっていう風な考え方もベースにして、
ヒットしない場合は隣のバケットに順番に線形探索していくんですけど、
そういう風に考えた時に基本的にキャッシュのインデックスの値が被ってバケットの隣にどんどん詰めていくケースって、
新しく詰めるのはライトの経路で、読みに行くのは先頭から順番に読み出す経路あるわけなんですけど、
この読む経路に関しては基本的にロックフリーでできるっていうのが性質上あって、
書く時だけロックを取って書くわけなんですけど、書いてる時もキャッシュが外れて隣のバケット、隣のバケット、隣のバケットって検索していった先のところを書き込みに行くから、
基本的に一番最初のバケットにヒットして返してくるという前提とするんだったらそこには触らないわけですよね。
なので基本的に読み出しに関してはロックフリーでできるという特徴があって、ここまで考えるとスライスよりかはさすがにパフォーマンスは落ちちゃうんですけど、
そこまで劣化せずに型のアドレスから任意の書類を引くという処理が実現できるということに気づいて、
それで実装したっていうのがタイプリンクスを消せた要因ですね。
その2項でリンクネームが不要なような改善ができた、プラスアルファ速度の改善もできたというところなんですね。
前者があれですよね、語自体の進化によって可能になったところと、
後は後者に関してはアルゴリズムというかデータ構造の調整みたいなところですかね。
そうですね。
そういったところの工夫でメモリの削減とかもされて、速度も問題なくっていうところだったんですね。
ちなみにその後者のアイディアっていうのはこの2年間で温めてたアイディアなんですか?
後者は正直アイディアではなくて、コーディングエージェントと壁打ちしてる時に
リンクネームでタイプリンクだけど代替案を出してくれっていうようなところから始まって採用した案ですね。
すごい、じゃあAIとの壁打ちの中で出てきたアイディアなんですね。
はい、そうです。
やっぱりAIすごいですね、壁打ち相手として単純に。
そうですね、あと他にも詳しくは触れないんですけど、VM方式を取るにあたって
型からコードの列を生成する経路、かなり実装も複雑で難しかったんですけど、
それによってエンコーダーの特別な経路でバグになってしまったりとかいろいろあって、
それをどうにかロバットに処理できないかっていうのも同じように相談して実現できたみたいなのがあったりとか、
だいぶコーディングエージェントで助けられつつ実装したところはありますね。
だいぶ堅牢性みたいなのが上がったのが今回のバージョンからの特徴かなと思ってます。
velox-io/jsonのELF VMとcomputed goto
すごい、そういった形でSilver Weekに大幅アップデートしたGo.JSONはもうJSON界で一番早いんですかね、高速。
いや、そうですね。ソニックに勝てたことで一番早いと言えるだろうなって思ってたんですけど、
なんかイシューを整理してたら、このライブラリをベンチマークに加えてくれないかっていうイシューがあるのに気づいて、
それがVeloci.ioっていうところが出てるJSONっていうライブラリなんですけど、
それをちょっと試してみようかと思って、ベンチマークに載せるために自分のローカルで計測するってやってみたら、
基本的には載せてくれっていう方が遅いんですよ、Go.JSONより。
でも今回も遅いといいなと思ってやってみたら、のきなみ全部Go.JSONよりも早くて。
まさかの。
当然ソニックよりも全部早いみたいな。
すごい。
で、ここまででも割と普通なことが多くて、早くするだけだったら、何ですかね、変な話、全然誰でもできるというか、
早くして、かつみんなが安全に使えるっていうところに落とすのは難しくて、
よくあるんですけど、こういうライブラリ作ろうと思って作ってみたら最初はめっちゃ早かったけど、
こういうところでバグが出ますっていうイシューをいっぱい報告されて直していくうちに分岐が増えまくって遅くなってくるみたいな。
あるあるですね。
なので、ちゃんと動くかっていうところもすごい大事なところではあるんですけど、
このライブラリに限っては動いてそうな感じがしていると。
すごい。
で、じゃあそうなってくるとどうやって早くしてるんだろうっていうのは気になって、
実装をちょっと軽く読みに行ったんですけど、
ちょっと納得できるようなことをやってて、
このライブラリが面白いのは、事前にARM64とかAMD64とかのターゲットアーキテクチャ向けのコンパイル済みのELF形式とかのバイナリをリポジトリに置いておいて、
それをGoのエンベッドパッケージでライブラリのコンパイル、ソースコードに埋め込んでおいて、
それを実行時に、
ジャストインコンパイルとかと似た仕組みなんですけど、
メモリ上にELFのバイナリをバッと展開して、
そのセントアドレスにジャンプするとか呼び出すってバーっと評価していくみたいなことをやってるんですよ。
これはJITと違うのは、エンジン自体をもともとCで書いてて、
CをELFに行ったものをソースコードに埋め込んで、それをメモリに展開して、
GoからそのCのエンジンに直接ジャンプしてるみたいな、そういう使い方。
Cって言ったらまずはC5を思いつくと思うんですけど、
C5って結局GoとC5の間の関数呼び出しの間で、
いろんな規約の変換というか、いろいろやらないといけない処理があるので、
その分遅くなるんですよ。
でもこれはオーバーヘッドを直接メモリ展開したところに飛ぶっていう、
自分でそこの実装してるわけですよね、C5とか、
C5相当のことを自分でやってるみたいなアプローチで、
なのでそれでオーバーヘッドが無くして、Cのエンジンに移って帰ってくる。
またそのCのエンジンからGoも呼び出す処理が必要なんですけど、
そういう時も同様にC5を使わずに帰ってくるような処理を実現してるみたいなところが、
かなりトリッキーなことをしてて早くなってると。
すごいですね。
すごい雑に言ってしまうとあれですよね、
Goを側として使ってCのバイナリを実行してるわけではなくて、
Goの中にネイティブとしてCの世界を入れ込んでいるみたいなイメージですよね。
確かにそれが近いですね。
なのでオーバーヘッドを少なくCの実行ができるということを実現してると。
そうですね。話してなかったやつで言うと、
VM型のスイッチケースを評価していくときに、
C言語で書くんだったら一歩先の高速アプローチってのが取れて、
それがダイレクトスレッティングっていう名前がついてるアプローチになって、
例えばちょっとCの話になるんであれなんですけど、
Cのコンパイラー、例えばGCCのコンパイラーとかでは、
ラベルズバリューっていうGCC拡張があって、
Cのコンパイラーをするときに、
ケース文にGoToとかでジャンプするときのラベルにアドレスを付けられるというか、
このラベルのアドレスに直接GoTo文でジャンプするっていうのができるようになるんですよ。
これができるとどう面白いかっていうと、
ケース文として列挙してるケースのところを全部ラベルにして、
ケース1コロン、ケース2コロンみたいなラベルをダーッと付けるじゃないですか。
そのGoToするときに次のOPコードの次の命令のケース文に相当するラベルのアドレスに、
直接GoTo文でジャンプするみたいなことするとイメージできると思うんですけど、
ケース文の中から次のケース文にバンバン飛んでいくみたいなのが作れるんですよね。
そうするとフォア文で1回先頭に戻るみたいなのがなくて、早くなるみたいな。
直接行き来できるってことですよね。ケースの中からケースに。
そうです。こういうCだったらこういうテクニックを使って書いたコードをコンパイルしてバイナリにできるんだけど、
Goの世界でそれをやろうとするとGoのコンパイラーはそれに対応してないからできないみたいな、
そういう言語のコンパイラーの違いみたいなのがあるけど、
Cはそういうテクニックを使ってコンパイルした、キーでコンパイルしたバイナリっていうのを、
成果物だけリポジトリの中に持っておいて、それをGoの試算の中から呼び出すみたいな、
そういうアプローチをしてるのが面白いなと思ったところです。
なるほど。つまりGo.JSONはもっと早くなるってことですね。今のまとめと。
やろうとすればですけどね、ここまでやるかどうかっていうのもちょっと思ってて、
まだGo.JSONってやっぱりJITコンパイルにも対応してないんですけど、
それは対応できないからしてないんじゃなくて、してもいいものかをちょっと考えててやってないんですけど、
JITとかって、かなりこれを使って早くしちゃうと後戻りできないみたいなところがあって、
他のもっと先にチューニングするべきところがあるんじゃないかみたいなところからどんどん詰めていって、
もうどうにもこうにも使わなきゃいけなくなるぐらいの最後の手段にとっておいているっていうところがあって、
なのでまだ手を出してない領域なんですけど、それに結構近いなと思ってて、
Goの世界からだいぶ変わっちゃうので、CのコンパイラでコンパイルしたものをGoから使うっていう方向になると、
自分の中ではJITに近い、だいぶアーキテクチャーのかなりジャンプがあるので、
ここに手を出さずに何とかまだこのベロッサイをJSONに勝てないかっていうのをこれから考えていけないなと思っているところですね。
なるほど。ちょっと足は踏み入れずに若干検討しつつもできる限りのことで追いつけないかなというところが現状。
Cの話出てきたんで、PHPとCに詳しい方としては何かあったりします?ここに。
いや、このアプローチは自分も初めて聞いたんで全然知らないんですけど、PHPもCもそんなに詳しいわけじゃないですが。
そうですね、ELFバイナリーだとやっぱりLinuxしか使えないと思うんで、多分OSごとにあるってことですよね。
ですです。とか、ここのライブラリはターゲットのアプローチ以外は、アーキテクチャー以外は、
サポート、未サポートでパニックしたりとかしてるので、どこでも使えるみたいなGOの思想とは本当に曲げていっちゃってます。
アプローチとしては面白いけど、じゃあ実際サーバーとかで使うかって言うと難しいんじゃないかなって思っちゃいましたね。
GOの世界にいると、GOの型安全の機能とか、いろいろ安全に使える仕組みがいろいろあったりするので、
使わなきゃ早いだろうけど、安全性はなくなるんじゃないかなって思っちゃいましたね。
そうですね、まさに。
なるほど、GOの利点であるポータビリティっていうところが犠牲になってるってことなんですね。
Velox、iOS、JSONに関しては、現状の今のところは。
なるほど、そういうデメリットもあるっていうところで、
encoding/json/v2とコード生成の展望
ちょっとGOJSONの未来みたいなところの話も若干あったんで、今後のGOJSONどうなっていくのかみたいなところを聞いてもいいですか。
はい、まず皆さん待望なのかわかんないですけど、
エンコーディングJSON V2への対応っていうのを今やってて、
エンコーダー側はV2への対応が終わりましたと。
今まさにデコーダー側のV2対応っていうのをしているところでして、
ISCONが今月末にあると思うんですけど、
あと1週間ちょっと後ぐらいかにあると思うんですけど、
それに間に合うような形で出せないかっていうのを今頑張って実装しているところですね。
もしかしたらISCONのGOの実装がエンコーディングJSON V2ベースで書かれてる可能性すらあるんで、
そういう時にインポート分置き換えるだけで動かないじゃんっていう風になっちゃうのはちょっと悲しいので、
できることなら対応したいなって思っているのと、
あとはそのVeloc-SciをJSONに勝つためのっていうわけでもないんですけど、
別な高速化アプローチとしてコード生成方式に対応しようかなと思っていて、
今までJITとかの話でもあったように、
エンジンのチューニングっていうのを基本的にはずっとやり続けてきて、
他のアプローチっていうのをJITみたいに試しちゃうと、
戻ってこれなくなるっていう懸念があってやってなかったんですけど、
だいぶエンジン側のチューニングが終わってきたっていうところもあって、
晴れてこのコード生成方式へ対応しようかなと思っていて、
これがどういうものかっていうと、
EDJSONっていうちょっと古いライブラリがあるんですけど、
それがコード生成方式って言われてるようなライブラリで、
例えばエンコーダーだったら、
どういうエンコード処理が一番早いかっていうのは、
文字列をそのままソースコードにやっけておいて、
それをそのまま出力するのが一番早いっていう話をしたと思うんですけど、
まさにそれをやってるライブラリで、
まず最初に静的解析して、
ソースコード上にこの方をエンコードしたりとかっていうのを、
例えばマークしておいたりとか、
マークしてなくても対象とするソースコードをバーっと選んで、
自動的に見つけて、
その方に対するエンコード処理っていうのを、
自動生成で作るっていうアプローチですね。
自動生成してあるGoのコードを一緒にコンパイルするので、
当然、自動生成された最適化済みの、
もうこうしか動かないっていうようなコードが、
そこに焼き込まれた状態で一緒にコンパイルするんで、
当然一番理論上早くなる。
ですけど、GoってGenericsとかAnyとか、
動的に決まる方っていうのがあるわけですよ。
Genericsは実際にコンパイル時に解決されるものではあるんですけど、
静的解析みたいなアプローチだと、
Genericsがどう展開されるかって、
コンパイラーじゃないとわからないからできないわけですよね。
なので、GenericsとかAnyに対するエンコード処理、デコード処理っていうのは、
どうやったってランタイムのエンジンのパフォーマンスに依存してしまいますと。
なので、Go.JSONで今までエンジンのチューニングに重きを置いてたので、
ここに来てコード生成方式に対応することによって、
AnyとかGenericsのケースも早く処理できるし、
それ以外のケースはもっと早くできるようになる余剰があって、
これを使うことによって、アプローチが全然変わっちゃいますけど、
ベロック採用とか、ソニックとか、
他の高速なライブラリに差をつけられるんじゃないかなとは思う。
ありがとうございます。このコード生成方式の見込みみたいなのはどうなんでしょうか?
確実にこれでベロック採用JSONに勝てそうだなみたいな見込みあったりするんですか?
そうですね、当然勝てると思います。
というのも、結局JITだったり、いろんなテクニックを駆使するのって、
どうやってその動的に、
自動生成した型に特化した処理っていうのを、
自動生成のタイミングで作るわけですけど、
それにどうやって実行時の情報を駆使して近づけていくかっていうのが、
エンジンのチューニング方法になるので、理論値になるわけですよね。
コード生成したものが。なので、コード生成以外のアプローチを取っている以上、
どうやったってそこには勝てないかなというのはあります。
すごい。じゃあこれがリリースされた時にはかなり高速化されているし、
このコード生成リリースはさすがにISCOには間に合わない?
どうですかね。間に合わせたいなとは思ってますね。
頑張るっていうところですかね。
あとはよく高速化オプションがJSONに用意しているんですけど、
そういうのを使わずに利用されている方もちらほらいるように思えていて、
もうちょっと分かりやすく使えるといいのかなとか思ってましたね。
そういうオプションとか用意しようかなとか思ってます。
いいですね。じゃあこれを聞いているISCOに参加されて、
5を使う人はとりあえずインポート文を5C5JSONに変えれば、
とりあえずスコアは上がると。
上がるはず。
JSONが使われているんだったら。
まずはそこを見てくれっていうような感じですね、もうこれは完全に。
ありがとうございます。
ベンチマークとプロファイリングの向き合い方
結構いい時間になったのでここら辺で締められればなと思うんですけど、
またJSONから最後質問あったりします?
いやもう自分も全然5JSONの実装は全然分かってなかったので、
いろいろ聞けてよかったです。ありがとうございます。
すごい楽しかったですね、これ聞いていて。
すごい印象的だったのが5Cさんって割と理論派というか、
やっぱりアルゴリズムで詰めてくるなというところがかなり印象的に多かったですね。
計算量をめちゃくちゃ気にするじゃないですか。
多分話の中で何回も出てきたと思うんですよ。
オーダーができる限りオーダー位置に近づける。
絶対に指数完成的に計算量を伸ばさないみたいなところ。
かなりそこを意識していて、データ構造とかもちゃんとしっかりそれを意識したものにして、
ちゃんと速くするみたいなのがかなり意識的にされているのかなと思ったんですけど、
そういったところを意識してます?
そうですね、当然。
あとプロファイルとかをやっぱり頻繁に取るので、
どうやって高速並べられ作りたいんだったら、
どれだけベンチマークを充実させるか。
取ったベンチマークの結果が自分自身を過信させないようにするかというか、
さっき言ったバグがいっぱいあって1分を足していったら遅くなるみたいな、
そういうことにならないように、
できるだけ自分自身のライブラリのパフォーマンスが本当に正しいのかどうかを
自問自答しながら開発していけるような足回りというか、
プロファイリング環境とかベンチマークとかを用意して、
常にベンチの結果とかプロファイリングの結果と向き合っていくっていうのが大事かなと思いながら作ってましたね。
ありがとうございます。
最後にイスコン参加者でもいいですし、Go.JSONを使っているユーザーの方々でもいいですし、
これを聞いている人たちに向けてもいいんですけど、ご指摘の一言あればお願いします。
今回のお話の中で基本的にはエンコーダーに寄った話をずっとさせていただいたんですけど、
デコーダーにもいろんなチューニングというか入っていて、
JSONってエンコードとデコードが表裏一体で両方のパフォーマンスが必要になるわけなんですけど、
デコーダー側の話が漏れちゃったので気になる方はぜひソースコードを読んだりとか、
自分に何かしらで質問していただければ何でも回答しようと思うので、
興味を持っていただければ嬉しいと思っています。
ありがとうございました。
ありがとうございます。今回エンコーダーの話だけで1時間半弱ぐらい行ったので、
デコードの話もするともう1回行くので、合計3時間ぐらいの話になるかなという感じもするので、
もし機会があったら後編みたいな形でもう1回ゴシーさんに出ていただいて、
どっかの機会でもう1回お話できればなと思ってますので、ありがとうございました。
ということで今回はここまでにしたいなと思います。
ヤウヨルズのOSSでは毎回1つのOSSを取り上げて、それについて片付いて、
変得で技術的なところも深掘りしながら話をしていく番組です。
今回みたいにですね、ゴシーさんみたいな形でゲストをお呼びして、
その方が作っているOSSについてもちょっと深掘って話していくみたいなのも、
今後も引き続きやりたいなと思ってますので、
もし我こそはという人がいたらお声掛けください。よろしくお願いします。
ぜひお聞きのプラットフォームでフォローと高評価の方よろしくお願いします。
コメントとか、あとは感想とか、Xとかで呟いていただく際には、
ハッシュタグヤウヨルズのOSSつけてくださると見つけやすいのでよろしくお願いします。
それではゴシーさんありがとうございました。
ありがとうございました。
01:22:22

コメント

スクロール