大家好,我是程序员Spectre。DoltDB[1]是世界上第一个可以像git存储库一样进行分支和合并、推送和拉取、分叉和克隆的SQL数据库。我们从头开始构建Dolt的存储引擎以加速这些操作。编写行存储引擎和SQL执行引擎并不容易。大多数人甚至不尝试它是有原因的,但在DoltHub,我们以不同的方式构建它。今天,我们将回顾几个在对Dolt进行基准测试以使行访问与MySQL一样快时遇到的性能问题的案例研究。每个案例研究都是我们遇到并解决的真实性能问题。开始吧。案例研究:接口断言在尝试以更快的方式从磁盘中获取行并通过执行引擎时,我们决定创建一个新的行迭代接口。由于我们已经有许多原始接口的实现,我们认为我们可以像这样扩展它:)error}我们的SQL引擎的架构涉及一个深度嵌套的执行图,其中图中的每个节点都从下一层向下获取一行,对其进行一些处理,然后将其向上传递到链中。当我们在迭代器中实现Next2方法时,它看起来像这样:(sql.RowIter2).Next2(ctx,frame)}但当我们运行这段代码时,我们注意到在分析器图中,由于这种模式,我们付出了非常可观的性能损失。在我们的CPU图中,我们注意到在runtime.assertI2I上花费了大量时间:runtime.assertI2I是go运行时调用的一个方法,用于验证接口的静态类型是否可以在运行时转换为不同的类型。这涉及到接口表的查找和接口指针的转换,这不是那么昂贵,但肯定不是免费的。由于嵌套的执行图,我们在每行获取的数据中多次执行此操作,导致相当严重的性能损失。解决方案:消除接口类型转换为了消除这种损失,我们只需在需要消除转换的地方存储两个字段,每个静态类型一个。typeiterstruct{childItersql.RowIterchildIter2sql.RowIter2}func(titer)Next(...){returnt.childIter.Next(ctx)}func(titer)Next2(...){returnt.childIter2.Next2(ctx,frame)}这两个字段指向内存中的同一个对象,但是由于它们的静态类型不同,所以需要不同的字段来避免转换惩罚。案例研究:切片接下来,我们将研究分配切片的不同方式及其对性能的影响,尤其是垃圾收集器开销。我们将分析一种生成随机元素切片然后将它们相加的方法。我们会在个人资料中一遍又一遍地称呼它。funcsumArray()uint64{a:=randArray()varsumuint64fori:=rangea{sum+=uint64(a[i].(byte))}returnsum}我们将着眼于实现randArray()函数4种不同的方法,从最坏到最好,并展示了不同实现对程序性能的影响。最差:追加到切片获取该切片的更糟糕的方法是创建一个零长度切片,然后一遍又一遍地调用追加,如下所示:funcrandArray()[]interface{}{vara[]interface{}因为我:=0;我<1000;i++{a=append(a,byte(rand.Int()%255))}returna}我们是否从上面的nil切片开始,或者使用make([]interface{},0)来制作一个零长度切片,效果是一样的。当我们根据profile生成火焰图时,我们可以看到垃圾回收占用了巨大的开销,runtime.growslice占用了整整四分之一的CPU周期。总体而言,不到一半的CPU周期直接用于有用的工作。这么贵的原因是goslice有一个底层数组,当调用append时,如果底层数组容量不够,运行时不得不分配一个更大的数组,复制所有元素,并回收旧数组.我们可以做得更好。更好的是:静态数组大小我们可以通过在创建时固定切片的大小来消除runtime.growslice开销,就像这样。funcrandArray()[]interface{}{a:=make([]interface{},1000)fori:=0;我<长度(a);i++{a[i]=byte(rand.Int()%255)}returna}当我们剖析这段代码时,我们可以看到runtime.growslice的开销已经完全消除,垃圾收集器的压力已经减轻减少。您可以一眼看出此实现花费更多时间做有用的工作。但是每次创建切片仍然会对性能产生重大影响。我们的运行时间有整整13%用于分配切片,即runtime.makeslice。更好:原始切片类型奇怪的是,我们可以通过分配字节切片而不是切片interface{}来做得更好。执行此操作的代码:funcrandArray()[]byte{a:=make([]byte,1000)fori:=0;我<长度(a);i++{a[i]=byte(rand.Int()%255)}returna}当我们查看配置文件时,我们可以看到runtime.makesliceCPU的影响从13%以上下降到大约3%。您甚至在火焰图上也几乎看不到它,并且很容易看到垃圾收集器开销的相应减少。造成差异的原因很简单,interface{}类型的分配成本更高(一对8字节指针,而不是单个字节),而且垃圾收集器推理和处理它的成本也更高。这个故事的寓意是,在可能的情况下,分配原始切片类型而不是接口类型通常会在性能方面得到回报。最佳:在循环外分配,但实现此目的的唯一最佳方法是根本不在循环内分配任何内存。相反,让我们在外部作用域中分配一次切片,然后将其传递给此函数以进行填充。funcrandArray(a[]interface{}){fori:=0;我<长度(a);i++{a[i]=byte(rand.Int()%255)}}当我们这样做时,我们完全消除了所有垃圾收集压力,我们有效地将所有CPU周期用于有用的工作。我们不必将切片作为参数传入,我们只需要避免每次调用此函数时都分配它。实现此目的的另一种方法是使用全局sync.pool变量在函数调用之间重用切片。总结调用rand.Int花费的时间作为我们花费多少CPU时间做有用工作的例子,我们得到以下总结:appending:20%interface{}typestaticslice:38%staticbyteslice:62%sliceas参数:70%底线:如何使用切片会对程序的整体性能产生非常重大的影响。案例研究:Structvs.Pointer作为Receiver在实现接口时,关于是使用struct还是指向struct的指针作为receiver类型,在golang社区中一直存在着非常激烈的争论。意见不一。事实上,这两种方法都需要权衡取舍。将结构复制到堆栈通常比指针更昂贵,尤其是对于大型结构。但是将对象保留在堆栈而不是堆上可以避免对垃圾收集器造成压力,这在某些情况下会更快。从美学/设计的角度来看,也存在权衡取舍:有时确实有必要强制执行不变性语义,这可以通过结构接收器获得。让我们来说明您为大型结构支付的性能损失。我们将使用一个非常大的36个字段,并对其反复调用一个方法。类型bigStruct结构{v1,v2,v3,v4,v5,v6,v7,v8,v9uint64f1,f2,f3,f4,f5,f6,f7,f8,f9float64b1,b2,b3,b4,b5,b6,b7,b8,b9[]bytes1,s2,s3,s4,s5,s6,s7,s8,s9字符串}func(bbigStruct)randFloat()float64{x:=rand.Float32()switch{casex<.1:returnb.f1...}当我们一次又一次地分析这个方法时,我们可以看到我们付出了非常大的代价,35%的CPU周期,在一个叫做runtime.duffcopy的东西中。什么是运行时。复制品?在某些情况下,这就是go运行时复制大的连续内存块(通常是结构)的方式。之所以这样称呼,是因为duffcopy有点像Duff的设备,这是TomDuff在80年代发现的C编译器hack。他意识到您可以滥用C编译器通过交错循环和开关的构造来实现循环展开:registershort*to,*from;registercount;{registern=(count+7)/8;switch(count%8){case0:do{*to=*from++;案例7:*to=*from++;情况6:*to=*from++;案例5:*to=*from++;案例4:*to=*from++;案例3:*to=*from++;情况2:*to=*from++;情况1:*to=*from++;}而(--n>0);}}循环展开可以极大地加快速度,因为您不必在第一次通过循环时调用每个Check循环条件。Go的duffcopy实际上不是Duff的设备,但它是一个循环展开:编译器发出N条指令来复制内存,而不是使用循环来这样做。避免付出这种代价的方法是简单地使用指向结构的指针作为接收者,避免昂贵的内存复制操作。但是在概括此建议时要小心——您关于哪种技术性能更好的直觉可能是不正确的,因为它实际上取决于许多与您的应用程序不同的因素。归根结底,您确实需要分析备选方案以了解哪些备选方案的整体性能更好,包括垃圾收集的影响。总结要使Dolt与更成熟的数据库技术一样高效,我们还有很多工作要做,尤其是在查询计划方面。但是我们已经通过这些简单的优化将引擎的性能提高了2倍,并通过完全重写我们的存储层[2]将性能提高了3倍。在简单的基准测试中,我们现在的延迟大约是MySQL的2.5倍,但我们还没有完成。我们很高兴在接近1.0版时继续提高我们的数据库速度。原文链接:https://www.dolthub.com/blog/2022-10-14-golang-performance-case-studies/参考[1]DoltDB:https://doltdb.com/[2]并通过Complete重写我们的存储层以实现另外3倍的改进:https://www.dolthub.com/blog/2022-09-30-new-format-default/
