こんにちは、hachi8833です。ゴールデンウィーク中日いかがお過ごしでしたしょうか。
Gobyを完全に理解してから書こうとするといつまでたっても書けないので、悩んでいることも含めて書くことにしました。
- リポジトリ: goby-lang/goby
- 公式サイト: https://goby-lang.org/
Go言語のdefer
はかなり遅い
ごく最近、GobyのSlackで「ちょいとベンチマーク取ってみたら、改良できそうなところが目についた: Goのdefer
はコストが高いので、これを減らすだけで高速化できそう」という書き込みがありました。
それを受けてst0012さんが早速VMのスタックからdefer
を取り除きました。正直、私がVM部分のコードをまじまじと覗き込んだのはこれが初めてかもしれません。
...
- *sync.RWMutex
+ sync.RWMutex
}
func (s *stack) set(index int, pointer *Pointer) {
s.Lock()
- defer s.Unlock()
-
s.Data[index] = pointer
+
+ s.Unlock()
}
func (s *stack) push(v *Pointer) {
s.Lock()
- defer s.Unlock()
if len(s.Data) <= s.thread.sp {
s.Data = append(s.Data, v)
} else {
s.Data[s.thread.sp] = v
}
s.thread.sp++
+ s.Unlock()
}
...
defer s.Unlock()
を外してs.Unlock()
に置き換えています。ついでに*sync.RWMutex
のポインタもやめています。
ベンチマークの結果、基本的な演算が確かに速くなってます。Gobyの最適化は機能が揃ってからの予定ですが、こういうリファクタリングは歓迎ですね。
go test -run '^$' -bench '.' -benchmem -benchtime 2s ./... > .tmp_benchmarks
benchcmp current_benchmarks .tmp_benchmarks
benchmark old ns/op new ns/op delta
BenchmarkBasicMath/add-8 6500 5783 -11.03%
BenchmarkBasicMath/subtract-8 6461 5723 -11.42%
BenchmarkBasicMath/multiply-8 6484 5709 -11.95%
BenchmarkBasicMath/divide-8 6445 5768 -10.50%
benchmark old allocs new allocs delta
BenchmarkBasicMath/add-8 78 78 +0.00%
BenchmarkBasicMath/subtract-8 78 78 +0.00%
BenchmarkBasicMath/multiply-8 78 78 +0.00%
BenchmarkBasicMath/divide-8 78 78 +0.00%
benchmark old bytes new bytes delta
BenchmarkBasicMath/add-8 3792 3792 +0.00%
BenchmarkBasicMath/subtract-8 3792 3792 +0.00%
BenchmarkBasicMath/multiply-8 3792 3792 +0.00%
BenchmarkBasicMath/divide-8 3792 3792 +0.00%
と思ったら、さらに「defer
をさらに減らしてもっと速くした」「スレッドをAPI化してVMから切り出してみようと思う」と追い打ちが来ました。何それ凄い。
まだ私は全然追いきれてませんが、「こういうのはスレッドあたりのミューテックスやdefer
を1つだけにしておくのがいいんですよ」だそうです。
benchmarking 2fd6637efac2bf8c371f020ab1e95aa0f7606873
benchmarking 5cfab57be7564a02bf15dd8ac63b9e66685014d9
universal.x86_64-darwin17
benchmark old ns/op new ns/op delta
BenchmarkBasicMath/add-8 5950 5473 -8.02%
BenchmarkBasicMath/subtract-8 5884 5416 -7.95%
BenchmarkBasicMath/multiply-8 5967 5418 -9.20%
BenchmarkBasicMath/divide-8 5916 5502 -7.00%
benchmark old allocs new allocs delta
BenchmarkBasicMath/add-8 78 78 +0.00%
BenchmarkBasicMath/subtract-8 78 78 +0.00%
BenchmarkBasicMath/multiply-8 78 78 +0.00%
BenchmarkBasicMath/divide-8 78 78 +0.00%
benchmark old bytes new bytes delta
BenchmarkBasicMath/add-8 3792 3792 +0.00%
BenchmarkBasicMath/subtract-8 3792 3792 +0.00%
BenchmarkBasicMath/multiply-8 3792 3792 +0.00%
BenchmarkBasicMath/divide-8 3792 3792 +0.00%
Switched to branch 'thread-rework'
いずれにしろまだ作業中なので、今後が楽しみです。
最近のGoby
Ruby X Elixir Conf Taiwan 2018での発表
作者のst0012さんがRuby X Elixir Conf Taiwan 2018の2日目にGobyについて発表しました。なおst0012さんは台湾の方です。
This is the slide of my talk, which explains my visions of @goby_lang and my plans to achieve them#rubyconftw https://t.co/2DscVdB22t
— Stan (@_st0012) April 28, 2018
#rubyconftw @goby_lang pic.twitter.com/vePTGTek6p
— Harisankar P S (@coderhs) April 28, 2018
スライドをWikiに設置
前回の記事に掲載したGobyの紹介スライドをWikiに配置しました。GitHubのMarkdownにはスライドや動画の埋め込みができないので、考えた挙句画像だけ貼り、そこからリンクでスライドに飛ばすようにしました。
「アンスコ数字形式1_000_000
が欲しい」
#660で、1_000_000
みたいなリテラル形式が欲しいというリクエストがありました。ついでに0b001001
とか1.2e-3
なども欲しいという意見もあり、「いずれは全部取り入れたいけど今じゃない」とst0012さんが締めました。
Ripper
クラスを作ってみた
Gobyでいずれlinter的なことを標準で行うのであれば、RubyのRipperみたいな標準パーサーがいずれ必要になるだろうと思って、見よう見まねでよちよち作ってみました(#658)。Goby自身のlexerやparserに委譲するだけなのでそんなに大変ではないかなと思ったのですが、型変換が意外にめんどかった…
func builtInRipperClassMethods() []*BuiltinMethodObject {
return []*BuiltinMethodObject{
...
Name: "instruction",
Fn: func(receiver Object, sourceLine int) builtinMethodBody {
return func(t *thread, args []Object, blockFrame *normalCallFrame) Object {
if len(args) != 1 {
return t.vm.initErrorObject(errors.ArgumentError, sourceLine, "Expect 1 argument. got=%d", len(args))
}
arg := args[0]
switch arg.(type) {
case *StringObject:
default:
return t.vm.initErrorObject(errors.TypeError, sourceLine, errors.WrongArgumentTypeFormat, classes.StringClass, arg.Class().Name)
}
i, err := compiler.CompileToInstructions(arg.toString(), NormalMode)
if err != nil {
return t.vm.initErrorObject(errors.TypeError, sourceLine, errors.InternalError, classes.StringClass, errors.InvalidGobyCode)
}
return t.vm.convertToTuple(i)
}
},
...
func (vm *VM) convertToTuple(instSet []*bytecode.InstructionSet) *ArrayObject {
ary := []Object{}
for _, insts := range instSet {
hInsts := make(map[string]Object)
hInsts["name"] = vm.initStringObject(insts.Name())
hInsts["type"] = vm.initStringObject(insts.Type())
if insts.ArgTypes() != nil {
hInsts["arg_types"] = vm.getArgNameType(insts.ArgTypes())
}
ary = append(ary, vm.initHashObject(hInsts))
aInst := []Object{}
for _, ins := range insts.Instructions {
hInst := make(map[string]Object)
hInst["action"] = vm.initStringObject(ins.Action)
hInst["line"] = vm.initIntegerObject(ins.Line())
hInst["source_line"] = vm.initIntegerObject(ins.SourceLine())
anchor, _ := ins.AnchorLine()
hInst["anchor"] = vm.initIntegerObject(anchor)
aParams := []Object{}
for _, param := range ins.Params {
aParams = append(aParams, vm.initStringObject(param))
}
hInst["params"] = vm.initArrayObject(aParams)
if ins.ArgSet != nil {
hInsts["arg_set"] = vm.getArgNameType(ins.ArgSet)
}
aInst = append(aInst, vm.initHashObject(hInst))
}
hInsts["instructions"] = vm.initArrayObject(aInst)
ary = append(ary, vm.initHashObject(hInsts))
}
return vm.initArrayObject(ary)
}
func (vm *VM) getArgNameType(argSet *bytecode.ArgSet) *HashObject {
h := make(map[string]Object)
aName := []Object{}
for _, argname := range argSet.Names() {
aName = append(aName, vm.initStringObject(argname))
}
h["names"] = vm.initArrayObject(aName)
aType := []Object{}
for _, argtype := range argSet.Types() {
aType = append(aType, vm.initIntegerObject(argtype))
}
h["types"] = vm.initArrayObject(aType)
return vm.initHashObject(h)
}
instruction
メソッドの部分を抜粋してみました。配列の中にハッシュや別の配列が入る形になっています。Gobyの配列はGoのスライスを、ハッシュはmap
を使っていますが、使い分けが少々ややこしいです。
プルリクが通るかどうかはわかりませんが、正味2日ほどで作れたのはIDE(JetBrainsのGoland)という強い味方のおかげです。感謝です。私がRubyのようにC言語で書くとしたら何年かかるかわかったものではありません。適当ですが、mattnさんなら3時間もあれば作っちゃうんじゃないかしら。
とはいうもののまだ改良の余地はあって、何とGo言語にはソースの(定数の値ではなく)定数名を直接取得する手段がないらしいので、一部を以下のような直書きでしのいでいます。コンパイラ言語だししょうがないか。
func convertLex(t token.Type) string {
var s string
switch t {
case token.Asterisk:
s = "asterisk"
case token.And:
s = "and"
case token.Assign:
s = "assign"
...
default:
s = strings.ToLower(string(t))
}
return "on_" + s
}
いろいろ調べたところ、GoのStringer
というライブラリを使えば、型指定されている定数を元に定数名を取得する関数を生成できるようなのですが、試してみると定数値が1, 2, 3のような連続した数値(iota
とかいうのを使うと簡単にできる)でないとうまくいかない感じなので諦めました。
さらに調べたところ、go generate
コマンドをコードのコメントに書いておけば任意のUnixコマンドをトリガできるらしいので、それを使ってGobyのtoken.goからこのコードを自動生成するスクリプトを書くのがよさそうです。そのうちやります。
ただしgo build
では自動生成がトリガされないので、手動なりmake
なりシェルスクリプトなりでコンパイル前にgo generate
を実行しておく必要があるようです。確かにセキュリティを考えればそんなことしない方がいいでしょうね(go get
とかgo install
で知らないコマンドが実行されたらコワイ)。
その後st0012さんから、「#662でローダブルなライブラリをVMから切り出す作業が進められていて、Ripper
もそれと同じように配置すべきなので、そっちが終わったら再開しようか」とコメントがありました。(`Д´)ゞラジャー!!