手游下载网

手游下载网

当Go遇上了Lua,会发生什么

admin 111 49

分享

在GitHub玩耍时,偶然发现了gopher-lua,这是一个纯Golang实现的Lua虚拟机。我们知道Golang是静态语言,而Lua是动态语言,Golang的性能和效率各语言中表现得非常不错,但在动态能力上,肯定是无法与Lua相比。那么如果我们能够将二者结合起来,就能综合二者各自的长处了(手动滑稽。

在项目Wiki中,我们可以知道gopher-lua的执行效率和性能仅比C实现的bindings差。因此从性能方面考虑,这应该是一款非常不错的虚拟机方案。

HelloWorld

这里给出了一个简单的HelloWorld程序。我们先是新建了一个虚拟机,随后对其进行了DoString()解释执行lua代码的操作,最后将虚拟机关闭。执行程序,我们将在命令行看到"HelloWorld"的字符串。

packagemainimport("/yuin/gopher-lua")funcmain(){l:=()()iferr:=(`print("HelloWorld")`);err!=nil{panic(err)}}//HelloWorld

提前编译

在查看上述DoString()方法的调用链后,我们发现每执行一次DoString()或DoFile(),都会各执行一次parse和compile。

func(ls*LState)DoString(sourcestring)error{iffn,err:=(source);err!=nil{returnerr}else{(fn)(0,MultRet,nil)}}func(ls*LState)LoadString(sourcestring)(*LFunction,error){((source),"string")}func(ls*LState)Load(,namestring)(*LFunction,error){chunk,err:=(reader,name)//proto,err:=Compile(chunk,name)//}

从这一点考虑,在同份Lua代码将被执行多次(如在httpserver中,每次请求将执行相同Lua代码)的场景下,如果我们能够对代码进行提前编译,那么应该能够减少parse和compile的开销(如果这属于hotpath代码)。根据Benchmark结果,提前编译确实能够减少不必要的开销。

packageglua_testimport("bufio""os""strings"lua"/yuin/gopher-lua""/yuin/gopher-lua/parse")//编译lua代码字段funcCompileString(sourcestring)(*,error){reader:=(source)chunk,err:=(reader,source)iferr!=nil{returnnil,err}proto,err:=(chunk,source)iferr!=nil{returnnil,err}returnproto,nil}//编译lua代码文件funcCompileFile(filePathstring)(*,error){file,err:=(filePath)()iferr!=nil{returnnil,err}reader:=(file)chunk,err:=(reader,filePath)iferr!=nil{returnnil,err}proto,err:=(chunk,filePath)iferr!=nil{returnnil,err}returnproto,nil}funcBenchmarkRunWithoutPreCompiling(b*){l:=()fori:=0;;i++{_=(`a=1+1`)}()}funcBenchmarkRunWithPreCompiling(b*){l:=()proto,_:=CompileString(`a=1+1`)lfunc:=(proto)fori:=0;;i++{(lfunc)_=(0,,nil)}()}//goos:darwin//goarch:amd64//pkg:glua//BenchmarkRunWithoutPreCompiling-810000019392ns/op85626B/op67allocs/op//BenchmarkRunWithPreCompiling-810000001162ns/op2752B/op8allocs/op//PASS//

虚拟机实例池

在同份Lua代码被执行的场景下,除了可使用提前编译优化性能外,我们还可以引入虚拟机实例池。

因为新建一个Lua虚拟机会涉及到大量的内存分配操作,如果采用每次运行都重新创建和销毁的方式的话,将消耗大量的资源。引入虚拟机实例池,能够复用虚拟机,减少不必要的开销。

funcBenchmarkRunWithoutPool(b*){fori:=0;;i++{l:=()_=(`a=1+1`)()}}funcBenchmarkRunWithPool(b*){pool:=newVMPool(nil,100)fori:=0;;i++{l:=()_=(`a=1+1`)(l)}}//goos:darwin//goarch:amd64//pkg:glua//BenchmarkRunWithoutPool-810000129557ns/op262599B/op826allocs/op//BenchmarkRunWithPool-810000019320ns/op85626B/op67allocs/op//PASS//

Benchmark结果显示,虚拟机实例池的确能够减少很多内存分配操作。

下面给出了README提供的实例池实现,但注意到该实现在初始状态时,并未创建足够多的虚拟机实例(初始时,实例数为0),以及存在slice的动态扩容问题,这都是值得改进的地方。

typelStatePoolstruct{[]*}func(pl*lStatePool)Get()*{()()n:=len()ifn==0{()}x:=[n-1]=[0:n-1]returnx}func(pl*lStatePool)New()*{L:=()//settingtheLuphere.//loadscripts,setglobalvariables,sharechannels,etcreturnL}func(pl*lStatePool)Put(L*){()()=app(,L)}func(pl*lStatePool)Shutdown(){for_,L:={()}}//GlobalLStatepoolvarluaPool=lStatePool{saved:make([]*,0,4),}

模块调用

gopher-lua支持Lua调用Go模块,个人觉得,这是一个非常令人振奋的功能点,因为在Golang程序开发中,我们可能设计出许多常用的模块,这种跨语言调用的机制,使得我们能够对代码、工具进行复用。

当然,除此之外,也存在Go调用Lua模块,但个人感觉后者是没啥必要的,所以在这里并没有涉及后者的内容。

packagemainimport("fmt"lua"/yuin/gopher-lua")constsource=`localm=require("gomodule")()print()`funcmain(){L:=()()("gomodule",load)iferr:=(source);err!=nil{panic(err)}}funcload(L*)int{mod:=((),exports)(mod,"name",("gomodule"))(mod)return1}varexports=map[string]{"goFunc":goFunc,}funcgoFunc(L*)int{("golang")return0}//golang//gomodule

变量污染

当我们使用实例池减少开销时,会引入另一个棘手的问题:由于同一个虚拟机可能会被多次执行同样的Lua代码,进而变动了其中的全局变量。如果代码逻辑依赖于全局变量,那么可能会出现难以预测的运行结果(这有点数据库隔离性中的“不可重复读”的味道)。

全局变量

如果我们需要限制Lua代码只能使用局部变量,那么站在这个出发点上,我们需要对全局变量做出限制。那问题来了,该如何实现呢?

我们知道,Lua是编译成字节码,再被解释执行的。那么,我们可以在编译字节码的阶段中,对全局变量的使用作出限制。在查阅完Lua虚拟机指令后,发现涉及到全局变量的指令有两条:GETGLOBAL(Opcode5)和SETGLOBAL(Opcode7)。

到这里,已经有了大致的思路:我们可通过判断字节码是否含有GETGLOBAL和SETGLOBAL进而限制代码的全局变量的使用。至于字节码的获取,可通过调用CompileString()和CompileFile(),得到Lua代码的FunctionProto,而其中的Code属性即为字节码slice,类型为[]uint32。

在虚拟机实现代码中,我们可以找到一个根据字节码输出对应OpCode的工具函数。

//获取对应指令的OpCodefuncopGetOpCode(instuint32)int{returnint(inst26)}

有了这个工具函数,我们即可实现对全局变量的检查。

packagemain//funcCheckGlobal(proto*)error{for_,code:={switchopGetOpCode(code){_GETGLOBAL:("notallowtoaccessglobal")_SETGLOBAL:("notallowtosetglobal")}}//对嵌套函数进行全局变量的检查for_,nestedProto:={iferr:=CheckGlobal(nestedProto);err!=nil{returnerr}}returnnil}funcTestCheckGetGlobal(t*){l:=()proto,_:=CompileString(`print(_G)`)iferr:=CheckGlobal(proto);err==nil{()}()}funcTestCheckSetGlobal(t*){l:=()proto,_:=CompileString(`_G={}`)iferr:=CheckGlobal(proto);err==nil{()}()}

模块

除变量可能被污染外,导入的Go模块也有可能在运行期间被篡改。因此,我们需要一种机制,确保导入到虚拟机的模块不被篡改,即导入的对象是只读的。

在查阅相关博客后,我们可以对Table的__newindex方法的修改,将模块设置为只读模式。

packagemainimport("fmt""/yuin/gopher-lua")//设置表为只读funcSetReadOnly(l*,table*)*{ud:=()mt:=()//设置表中域的指向为(mt,"__index",table)//限制对表的更新操作(mt,"__newindex",(func(state*)int{("notallowtomodifytable")return0}))=mtreturnud}funcload(l*)int{mod:=((),exports)(mod,"name",("gomodule"))//设置只读(SetReadOnly(l,mod))return1}varexports=map[string]{"goFunc":goFunc,}funcgoFunc(l*)int{("golang")return0}funcmain(){l:=()("gomodule",load)//尝试修改导入的模块iferr:=(`localm=require("gomodule");="helloworld"`);err!=nil{(err)}()}//string:1:notallowtomodifytable

写在最后

Golang和Lua的融合,开阔了我的视野:原来静态语言和动态语言还能这么融合,静态语言的运行高效率,配合动态语言的开发高效率,想想都兴奋(逃。

在网上找了很久,发现并没有关于Go-Lua的技术分享,只找到了一篇稍微有点联系的文章(京东三级列表页持续架构优化—Golang+Lua(OpenResty)最佳实践),且在这篇文章中,Lua还是跑在C上的。由于信息的缺乏以及本人(学生党)开发经验不足的原因,并不能很好地评价该方案在实际生产中的可行性。因此,本篇文章也只能当作“闲文”了,哈哈。

天下数据是国内屈指可数的拥有多处海外自建机房的新型IDC服务商,被业界公认为“中国IDC行业首选品牌”。

天下数据与全球近120多个国家顶级机房直接合作,包括香港、美国、韩国、日本、台湾、新加坡、荷兰、法国、英国、德国、埃及、南非、巴西、印度、越南等国家和地区的服务器、云服务器的服务.