self -comperated errors.new()和fmt.errorf()只能记录字符串信息。该错误只能通过日志提供。如果您需要获取呼叫堆栈,则需要其他代码逻辑。即使添加“%W”支持后,也没有基本的更改。
PKG/错误已经弥补了该短板非常令人兴奋,并且其withStack()提供了完整的呼叫堆栈信息,并且Wrap()还提供了更详细的错误路径跟踪功能。
我们只是对PKG/错误进行基准测试:
从基准测试结果可以看出,只有10级PKG/错误需要将近10US。和errors.new()在相同条件下仅损失150N。如果Error()输出少于30N,则差距很大。这就是为什么许多地鼠不愿使用pkg/errors替换errors.new()和fmt.errorf()的原因。
withStack()的冗余信息不应太多,但是wrap()接口并不是必需的,以携带完整的呼叫堆栈,因为实际上大多数withstack()实际上是重复的,并且如果您几次调用包装更多次()很容易引起长log。
许多学生无法忍受错误处理方法的问题!= nil {... return},并使用更方便的延期... panic(err)方法。尽管此方法可以轻松处理错误,但它也会带来致命的问题 - 如果您忘记了defer recountion(),它将导致退出的整个程序。因为这是许多团队列出了延期...恐慌(ERR)方法为红线,并且严格禁止触摸它。
可以看出,pkg/错误实际上使用了运行时。呼叫器获得完整的呼叫堆栈,并且此功能的实际性能差。简单的基准测试如下:
从测试结果可以看出,堆叠的损失接近5U。
作者的上一篇文章还涉及runtime.callers的优化。尽管本文仅优化了获得当前代码线的性能,但其想法仍然可以用作该优化的参考。
进行简单的基准测试:
从测试结果中,缓存的效果仍然很明显,从4000N到约1000N。
让我们先看一下Golang的呼叫堆栈。下图来自Cao Chunhui老师的GitHub文章:
我们在调用堆栈中看到两个关键信息----“父返回地址”和“呼叫者BP(呼叫者框架指针)”,分别表示当前功能的堆栈帧的返回PC和堆栈基础。
通过这两者的合作,我们可以得到一个完整的通话堆栈:
但是,我们知道,为了提高函数调用的性能,Golang将进行合格的小型功能的内部组合。因此,我们实际上可以通过上面的构建列表函数获得完整的调用堆栈。
我们可以找到扩展内部连接函数的代码,以在Golang的源代码中获取PC代码:
可以看出,它在_funcdata_inltree中找到了内部连接函数,并将其添加到PC列表中。我们也可以复制它,但不是必需的。显然,由buildstack()获得的PC列表和Runstack()和Runtime.caller()绝对是一对一的 - 一对一(如果有不同的意见,欢迎讨论),因此我们可以通过缓存。
使用缓存的代码改进性能如下:
再次进行简单的基准测试:
基准测试的结果仍然令人满意。从近4000NS到大约50N,将拨打堆栈的时间损失优化。
在这一点上,我们已经达到了提高呼叫堆栈性能的目标。
但是,这种优化方法也处于不利地位。它是依靠呼叫堆栈上的BP信息,并且可以看到GO的代码可以看到Golang的BP信息仅在AMD64和ARM64平台上可用。平台,我们还可以通过“ Debug/Dwarf”软件包读取Golang的矮人信息,以分析PC映射到堆栈框架。然而,差异很大。
对于withStack(),您可以压缩堆栈信息。
与PKG/错误的印刷信息相比,您可以感觉到更直观的感觉:
对于Wrap(),您只能保留一系列源信息。
例如:
以上代码调用堆栈将格式化为:
当然,该日志结构可以自身定义。关键是减少无效和重复的信息量。
让我们首先测试PKG/错误的性能以生成字符串:
基准测试结果表明,PKG/错误打印错误堆栈也造成了巨大的损失,多达8US。因此,您仍然必须找到一种改善它的方法。
作者知道,只有两种方法可以提高序列化性能,一种是提高序列化速度,另一个是减少内存复制的数量。显然,当缝线字符串时,前者不再是优化的,并且只能进行优化,并且可以想想后者的方式。
因此,作者在尝试了两种“引入缓冲区”和“提前计算字符串长度”的方法后选择了后者。
它的实施如下:
从结果来看,在当前场景中,性能比字节更快。新曲折,这令人惊讶。
作者的想法是检查Goroutine是否已在Panic(err)之前执行了defer catch()。如果没有执行,则不执行恐慌(ERR)。
代码显示如下:
该原理非常简单,即执行newguard()在执行defer函数(){}之前执行newguard()。在try(err)函数中获取标签到当前goroutine。在恐慌面前(err)。如果标签不是标签,请打印错误消息并退出当前的goroutine。由于恐慌,这比整个程序的出口轻得多。
我曾经想实现这样的界面:
显然,此接口是指go2.0的错误处理方法:
实施代码如下:
这里的实现原理也很简单,即在newtag()函数中,将newtag()函数的返回地址(pc)记录到tag.pc.pc.pc.pc.当tag.try(err)lock laster -up -up,如果呃!= nil,返回地址被tag.pc替换,因此它将跳到newtag()的返回以继续执行。= nil此时,如果逻辑并执行返回,则自然会进入允许该函数提前返回的情况。
但是,尽管这可以确保其按预期实施,但是存在一个更致命的问题,即在调试模式下对延期函数的执行期望和发布模式不一致。
例如,以下示例:
在调试模式下打印“ 12”,仅在发布模式下仅打印“ 1”。原因是在调试模式下,Golang编译器不会优化延期函数。当执行到延期时,延期函数挂在g._defer链接表上。在发布模式下,Golang编译器将在返回前(即,ret)命令的延期函数连接延期函数。我们更换了tag.try(err)to newTag()的返回地址的返回地址,但没有在newTag()之后添加defer函数到返回命令,因此newTag()之后的defer函数可以是subleimplement。
但是,还有一些方法可以处理它,也就是说,比较“反人类”。
上述代码的实践是用goto标签包装功能主体,以便Golang编译器将取消延期的内部链接优化,并将defer函数悬挂在G._defer链接列表上,以便代码与该代码一致在发行版和depugessenceif下完成的结果,此界面太不友好了,因此这只是一个“失败探索”
由相同原因以及以下示例引起的“失败探索”:
上面的代码是我要通过G._defer链接列表来检查是否有defer cach()在panic(err)之前。正确的g._defer链接列表。
基于上述优化想法,我编写了一个错误库LXT1045/错误。目前,这只是一个玩具。欢迎来到呕吐。
让我们看一下呼叫堆栈输出格式:
可以看出,与PKG/ERR相比,日志格式仍然有所改善。
再次进行简单的基准测试:
可以看出,与PKG/错误相比,将近10-20次存在差距,而且改进仍然很明显。
原始:https://juejin.cn/post/7121929424148103198