ホームページ >バックエンド開発 >Golang >Golang と Lua が出会ったとき、どんな火花が生まれるでしょうか?

Golang と Lua が出会ったとき、どんな火花が生まれるでしょうか?

藏色散人
藏色散人転載
2021-11-09 16:03:293723ブラウズ

この記事は go language チュートリアル コラムでご紹介したもので、Golang と Lua、困っている友達のお役に立てれば幸いです。

GitHub で遊んでいたときに、純粋な Golang で実装された Lua 仮想マシンである gopher-lua を偶然発見しました。 Golang が静的言語であるのに対し、Lua は動的言語であることはわかっています。Golang のパフォーマンスと効率は他の言語の中でも非常に優れていますが、動的な機能という点では、明らかに Lua に匹敵しません。したがって、この 2 つを組み合わせることができれば、それぞれの長所を組み合わせることができます (マニュアルはおかしいです。

プロジェクト Wiki では、gopher-lua の実行効率とパフォーマンスが C で実装されたものよりも優れているだけであることがわかります)バインディングが不十分であるため、パフォーマンスの観点から見ると、これは非常に優れた仮想マシン ソリューションとなるはずです。

Hello World

これは、単純な Hello World プログラムです。まず新しい仮想マシンを作成し、その上で DoString(...) を実行して lua コードを解釈して実行し、最後に仮想マシンを閉じます。プログラムを実行すると、コマンドに「Hello World」という文字が表示されます。 line String.

package main
import (
"github.com/yuin/gopher-lua"
)
func main() {
l := lua.NewState()
defer l.Close()
if err := l.DoString(`print("Hello World")`); err != nil {
panic(err)
}
}
// Hello World

事前にコンパイル済み

上記の DoString(...) メソッドの呼び出しチェーンを確認したところ、毎回 DoString(. ..) または DoFile(...) は、解析とコンパイルをそれぞれ 1 回ずつ実行します。

func (ls *LState) DoString(source string) error {
if fn, err := ls.LoadString(source); err != nil {
return err
} else {
ls.Push(fn)
return ls.PCall(0, MultRet, nil)
}
}
func (ls *LState) LoadString(source string) (*LFunction, error) {
return ls.Load(strings.NewReader(source), "<string>")
}
func (ls *LState) Load(reader io.Reader, name string) (*LFunction, error) {
chunk, err := parse.Parse(reader, name)
// ...
proto, err := Compile(chunk, name)
// ...
}

ここから考えると、同じ Lua コードが複数回実行されることになります (http サーバーなどでは、各リクエストが実行されます)同じ Lua コード) シナリオでは、事前にコードをコンパイルできれば、解析とコンパイルのオーバーヘッドを削減できるはずです (これがホットパス コードの場合)。ベンチマークの結果によると、早期にコンパイルすると確かに不必要なオーバーヘッドを削減できます。

package glua_test
import (
"bufio"
"os"
"strings"
lua "github.com/yuin/gopher-lua"
"github.com/yuin/gopher-lua/parse"
)
// 编译 lua 代码字段
func CompileString(source string) (*lua.FunctionProto, error) {
reader := strings.NewReader(source)
chunk, err := parse.Parse(reader, source)
if err != nil {
return nil, err
}
proto, err := lua.Compile(chunk, source)
if err != nil {
return nil, err
}
return proto, nil
}
// 编译 lua 代码文件
func CompileFile(filePath string) (*lua.FunctionProto, error) {
file, err := os.Open(filePath)
defer file.Close()
if err != nil {
return nil, err
}
reader := bufio.NewReader(file)
chunk, err := parse.Parse(reader, filePath)
if err != nil {
return nil, err
}
proto, err := lua.Compile(chunk, filePath)
if err != nil {
return nil, err
}
return proto, nil
}
func BenchmarkRunWithoutPreCompiling(b *testing.B) {
l := lua.NewState()
for i := 0; i < b.N; i++ {
_ = l.DoString(`a = 1 + 1`)
}
l.Close()
}
func BenchmarkRunWithPreCompiling(b *testing.B) {
l := lua.NewState()
proto, _ := CompileString(`a = 1 + 1`)
lfunc := l.NewFunctionFromProto(proto)
for i := 0; i < b.N; i++ {
l.Push(lfunc)
_ = l.PCall(0, lua.MultRet, nil)
}
l.Close()
}
// goos: darwin
// goarch: amd64
// pkg: glua
// BenchmarkRunWithoutPreCompiling-8         100000             19392 ns/op           85626 B/op         67 allocs/op
// BenchmarkRunWithPreCompiling-8           1000000              1162 ns/op            2752 B/op          8 allocs/op
// PASS
// ok      glua    3.328s

仮想マシン インスタンス プール

同じ Lua コードが実行されるシナリオでは、事前コンパイルを使用してパフォーマンスを最適化することに加えて、仮想マシン インスタンス プールを導入することもできます。

Lua 仮想マシンの新規作成には大量のメモリ割り当て操作が含まれるため、実行のたびに再作成および破棄する方法を採用すると、大量のリソースが消費されます。仮想マシン インスタンス プールは、仮想マシンを再利用できるため、不要なオーバーヘッドが削減されます。

func BenchmarkRunWithoutPool(b *testing.B) {
for i := 0; i < b.N; i++ {
l := lua.NewState()
_ = l.DoString(`a = 1 + 1`)
l.Close()
}
}
func BenchmarkRunWithPool(b *testing.B) {
pool := newVMPool(nil, 100)
for i := 0; i < b.N; i++ {
l := pool.get()
_ = l.DoString(`a = 1 + 1`)
pool.put(l)
}
}
// goos: darwin
// goarch: amd64
// pkg: glua
// BenchmarkRunWithoutPool-8          10000            129557 ns/op          262599 B/op        826 allocs/op
// BenchmarkRunWithPool-8            100000             19320 ns/op           85626 B/op         67 allocs/op
// PASS
// ok      glua    3.467s

ベンチマークの結果は、仮想マシン インスタンス プールが実際に多くのメモリ割り当て操作を削減できることを示しています。

インスタンス プールの実装READMEで提供されている内容を以下に示しますが、この実装では初期状態では十分な仮想マシンインスタンスが作成されていない(初期インスタンス数は0)ことと、スライスの動的拡張の問題があった点に注意してください。

type lStatePool struct {
    m     sync.Mutex
    saved []*lua.LState
}
func (pl *lStatePool) Get() *lua.LState {
    pl.m.Lock()
    defer pl.m.Unlock()
    n := len(pl.saved)
    if n == 0 {
        return pl.New()
    }
    x := pl.saved[n-1]
    pl.saved = pl.saved[0 : n-1]
    return x
}
func (pl *lStatePool) New() *lua.LState {
    L := lua.NewState()
    // setting the L up here.
    // load scripts, set global variables, share channels, etc...
    return L
}
func (pl *lStatePool) Put(L *lua.LState) {
    pl.m.Lock()
    defer pl.m.Unlock()
    pl.saved = append(pl.saved, L)
}
func (pl *lStatePool) Shutdown() {
    for _, L := range pl.saved {
        L.Close()
    }
}
// Global LState pool
var luaPool = &lStatePool{
    saved: make([]*lua.LState, 0, 4),
}

モジュール呼び出し

gopher-lua は、Lua による Go モジュールの呼び出しをサポートしています。個人的には、これは非常に魅力的な機能だと思います。開発では、一般的に使用される多くのモジュールを設計することがあり、この言語間呼び出しメカニズムにより、コードとツールの再利用が可能になります。

もちろん、この他にも Lua モジュールを呼び出す Go もありますが、後者は個人的には必要ないと思うので、ここでは扱いません。

package main
import (
"fmt"
lua "github.com/yuin/gopher-lua"
)
const source = `
local m = require("gomodule")
m.goFunc()
print(m.name)
`
func main() {
L := lua.NewState()
defer L.Close()
L.PreloadModule("gomodule", load)
if err := L.DoString(source); err != nil {
panic(err)
}
}
func load(L *lua.LState) int {
mod := L.SetFuncs(L.NewTable(), exports)
L.SetField(mod, "name", lua.LString("gomodule"))
L.Push(mod)
return 1
}
var exports = map[string]lua.LGFunction{
"goFunc": goFunc,
}
func goFunc(L *lua.LState) int {
fmt.Println("golang")
return 0
}
// golang
// gomodule

変数汚染

インスタンス プールを使用してオーバーヘッドを削減すると、別のやっかいな問題が発生します。同じ仮想マシンが複数回 Lua コードを実行される可能性があるためです。そしてその中のグローバル変数を変更します。コード ロジックがグローバル変数に依存している場合、予測できない実行結果が発生する可能性があります (これは、データベース分離における「反復不可能な読み取り」に少し似ています)。

グローバル変数

Lua コードがローカル変数のみを使用するように制限する必要がある場合は、この出発点からグローバル変数を制限する必要があります。そこで問題は、どうやってそれを達成するかということです。

Lua はバイトコードにコンパイルされ、解釈されて実行されることがわかっています。次に、バイトコードのコンパイル段階でグローバル変数の使用を制限できます。 Lua 仮想マシンの命令を確認したところ、グローバル変数に関係する 2 つの命令、GETGLOBAL (オペコード 5) と SETGLOBAL (オペコード 7) があることがわかりました。

この時点で、すでに一般的なアイデアが得られています。バイトコードに GETGLOBAL と SETGLOBAL が含まれているかどうかを判断することで、コード内でのグローバル変数の使用を制限できます。バイトコードの取得に関しては、CompileString(...) と CompileFile(...) を呼び出すことで Lua コードの FunctionProto を取得できます。Code 属性は []uint32 型のバイトコード スライスです。

仮想マシンの実装コードには、バイトコードに従って対応する OpCode を出力するツール関数があります。

// 获取对应指令的 OpCode
func opGetOpCode(inst uint32) int {
return int(inst >> 26)
}

このツールの機能を使用すると、グローバル変数を確認できます。

package main
// ...
func CheckGlobal(proto *lua.FunctionProto) error {
for _, code := range proto.Code {
switch opGetOpCode(code) {
case lua.OP_GETGLOBAL:
return errors.New("not allow to access global")
case lua.OP_SETGLOBAL:
return errors.New("not allow to set global")
}
}
// 对嵌套函数进行全局变量的检查
for _, nestedProto := range proto.FunctionPrototypes {
if err := CheckGlobal(nestedProto); err != nil {
return err
}
}
return nil
}
func TestCheckGetGlobal(t *testing.T) {
l := lua.NewState()
proto, _ := CompileString(`print(_G)`)
if err := CheckGlobal(proto); err == nil {
t.Fail()
}
l.Close()
}
func TestCheckSetGlobal(t *testing.T) {
l := lua.NewState()
proto, _ := CompileString(`_G = {}`)
if err := CheckGlobal(proto); err == nil {
t.Fail()
}
l.Close()
}

モジュール

汚染される可能性のある変数に加えて、インポートされた Go モジュールも実行時に改ざんされる可能性があります。したがって、仮想マシンにインポートされたモジュールが改ざんされていないこと、つまりインポートされたオブジェクトが読み取り専用であることを保証するメカニズムが必要です。

関連ブログを参照した後、Table の __newindex メソッドを変更し、モジュールを読み取り専用モードに設定できます。

package main
import (
"fmt"
"github.com/yuin/gopher-lua"
)
// 设置表为只读
func SetReadOnly(l *lua.LState, table *lua.LTable) *lua.LUserData {
ud := l.NewUserData()
mt := l.NewTable()
// 设置表中域的指向为 table
l.SetField(mt, "__index", table)
// 限制对表的更新操作
l.SetField(mt, "__newindex", l.NewFunction(func(state *lua.LState) int {
state.RaiseError("not allow to modify table")
return 0
}))
ud.Metatable = mt
return ud
}
func load(l *lua.LState) int {
mod := l.SetFuncs(l.NewTable(), exports)
l.SetField(mod, "name", lua.LString("gomodule"))
// 设置只读
l.Push(SetReadOnly(l, mod))
return 1
}
var exports = map[string]lua.LGFunction{
"goFunc": goFunc,
}
func goFunc(l *lua.LState) int {
fmt.Println("golang")
return 0
}
func main() {
l := lua.NewState()
l.PreloadModule("gomodule", load)
    // 尝试修改导入的模块
if err := l.DoString(`local m = require("gomodule");m.name = "hello world"`); err != nil {
fmt.Println(err)
}
l.Close()
}
// <string>:1: not allow to modify table

最後に書きました

Golang と Lua の統合により、私の視野が広がりました。静的言語と動的言語をこの方法で統合できることがわかりました。静的言語は高速に動作します。動的言語開発の効率の高さと組み合わせると、考えるだけで興奮します (エスケープ。

)

長い間オンラインで検索しましたが、Go-Lua に関する技術的な共有がないことがわかりました。わずかに関連する記事 (JD.com のレベル 3 リスト ページの継続的アーキテクチャ最適化 - Golang Lua (OpenResty) のベスト プラクティスのみを見つけました) ). そしてこの記事では、Lua は依然として C 上で実行されます。情報が不足しており、私 (学生側) の開発経験が不足しているため、実際の運用環境でのこのソリューションの実現可能性を十分に評価できません。なので、この記事は「カジュアル記事」としか思えません(笑)。

以上がGolang と Lua が出会ったとき、どんな火花が生まれるでしょうか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はjuejin.imで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。