toolb 显示bp.映射不是一个函数?

本文章向大家介绍朴素贝叶斯模型(NBM)详解与在Matlab和Python里的具体应用,主要内容包括1、 FullBNT简介、基于Matlab的贝叶斯网络工具箱BNT是//item/21db99f36d90bc49932af29d、BNT中提供了较为丰富的结构学习函数,都有:、参数学习算法函数、推理机制及推理引擎、为了提高运算速度,使各种推理算法能够有效应用,BNT工具箱采用了引擎机制,不同的引擎根据不同的算法来完成模型转换、细化和求解。这个推理过程如下。、5、 参数学习、在BNT中,参数评估程序可以分为4类。分类依据是否目标是通过参数或仅仅一个点的估计来计算贝叶斯全部的后验概率,是否全部的节点是可观察的或者存在数据/隐含变量(局部可观察)。、先验参数分布、6、结构学习、K2算法、K2算法(Cooper and Herskovits, 1992)是一种按如下方式工作的贪婪搜索算法。每一个起始点没有父节点。然后增加结果结构打分最高时的父节点。当单独添加父节点再不能提高分数时,停止添加父节点。当我们使用固定的顺序时,我们不需要做循环检查,也不需要为每个节点单独选择父节点。BNT推广了这点允许使用任何种类的CPD,无论贝叶斯打分规则还是BIC,另外,你可以对每一个节点指定一个任意的父节点数量的上限。、推断引擎、全局推理算法、近似传播引擎、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

今天给大家介绍机器学习的一种分类模型朴素贝叶斯模型,这是我整理了好久的文章,希望大家能学到一点知识我也是欣慰的^_^o~ 努力!

点击阅读原文可获得工具包连接与密码:sm2s

回复贝叶斯Matlab可获取全部文章 Word版

贝叶斯 Thomas Bayes,英国数学家。他首先将归纳推理法用于概率论基础理论,并创立了贝叶斯统计理论,对于统计决策函数、统计推断、统计的估算等做出了贡献。

贝叶斯决策理论是主观贝叶斯派归纳理论的重要组成部分。贝叶斯决策就是在不完全情报下,对部分未知的状态用主观概率估计,然后用贝叶斯公式对发生概率进行修正,最后再利用期望值和修正概率做出最优决策。其基本思想是:  

  •   1、已知类条件概率密度参数表达式和先验概率。
  •   2、利用贝叶斯公式转换成后验概率。
  •   3、根据后验概率大小进行决策分类。

  未知事件中A[i]出现时B[j]出现的后验概率在主观上等于已有事件中B[j]出现时A[i]出现的先验概率值乘以B[j]出现的先验概率值然后除以A[i]出现的先验概率值最终得到的结果。这就是贝叶斯的核心思想:用先验概率估计后验概率。

  具体到分类模型中,上述公式可以理解为:将B[j]看作分类的一种,将A[i]看作样本的特征属性之一,此时等号左边为待分类样本中出现特征A[i]时该样本属于类别B[j]的概率P(B[j]|A[i]),而等号右边是根据训练样本统计得到的特征A[i]出现子类别B[j]中的概率P(A[i]|B[j])乘以类别B[j]在训练样本中出现的概率P(B[j])最后除以特征A[i]在训练样本中出现的概率P(A[i])。

  以下为基本的概念介绍,有概率论知识基础的可以跳过,这部分主要是为一些不理解上面公式的初始学习者进行指导。

1.学习树扩展贝叶斯网络结构的TANC算法.

2.数据完整条件下学习一般贝叶斯网络结构学习算法

数据完整条件下贝叶斯结构算法

3.缺失数据条件下学习一般贝叶斯网络结构学习算法

缺失数据条件下贝叶斯结构算法

1.BNT中也提供了丰富的参数学习函数,都有:

3.数据缺失时,如果已知网络拓扑结构,用EM算法来计算参数, learn_params_em ()

BNT中提供了多种推理引擎,都有:

一个Noisy-or节点就像通常的“或”门一样,但有时父节点的效果将被抑制。受抑制的父节点i的概率用 来表示。一个节点C,有两个父节点A和B,有如下CPD,使用F和T来表达关和开,(在BNT中是1和2)。

神经网络节点 使用一个多层感知器实现了从连续父节点向离散子节点的映射。

高斯节点 将连续值的节点处理成一个离散的情况

我们将 N/(q*r) 放入每个格;N 是等效的样本大小,r=|A|,q = |B|. 这可以按如上面方式创建:

这里 1 是等效样本大小,也是先验概率的强度。你可以使用上面面方式更改它,

1.建立模型A->B,生成样本数据

5.正确的模型在12次后收敛


% 贝叶斯选择模型示例.
% 建立模型A->B,生成样本数据
% 正确的模型在12次后收敛
% 结构先验,假设为均匀分布
% 保存结果并初始化训练结构
% Plot,模型后验概率
 
 
图为贝叶斯模型选择后验概率对比
BNT中的结构学习程序可以按类似参数学习的情况分成四类:
 

 
如果两个 DAGs 编码同样的条件独立,它们被叫做 Markov 等效。所有 DAGs 的集合可以被分割成 Markov 等效类。同一类内的线图可以有方向,它们弧的颠倒不会改变任何 CI 关系。每一类都可以用一个 PDAG(partially directed acyclic graph,局部有向非循环图)这种图被称为本质图或方向图。这个详细说明哪个边必须在某一个方位上被定向,哪个可能被颠倒。

结构学习的强有力手段是列举DAGs的所有可能性,并对它们一一打分。这为其它算法的比较提供了一个“黄金标准”。我们按如下做:


默认的情况下,我们使用贝叶斯打分规则,并假定 CPDs 是用带有 BDeu 的先验表表示的。如果想是用一致的先验值,我们可以通过如下方法覆盖这些默认值。





实际上不能列举N>5的所有可能的DAGs。

 

 




爬山算法从状态空间中的一个指定点开始,考虑所有最接近的邻节点,然后移向得分最高的相邻节点。当相邻节点得分没有高于当前节点时(例如到达了局部最大值。),算法停止。然后从空间的其它部分重新开始。“相邻”通常定义为所有的图可以通过从当前的图添加、删除或翻转一个单独的弧得出,并服从无环的约束。其它相邻的可能详。


使用Metropolis-Hastings(MH)的马尔可夫链蒙特卡尔算法来搜索所有的图空间。标准的分配提案是考虑移动所有最近的按上面定义的邻节点。这个函数可以按如下方法调用:


计算贝叶斯打分时,有部分是计算具有挑战性的观测,因为参数学习的后验概率变成了多峰的状态(这是由于隐含节点导致了一个混合的分布)。因此需要使用逼近算法,如 BIC。不幸的是搜索算法仍然是代价高昂的,因为我们需要在每一步运行 EM 算法来计算 MLE
值,它需要对每一个模型进行计算打分。一个变换的方法是在每步进行局域搜索来替代第M步的 EM,当数据是“添满”状态时这种方法非常有效。——以上被称为结构上的 EM 算法(Friedman 1997)它可以通过 BIC 打分收敛的局部最大值来证明。

 
创立好一个贝叶斯网络,我们现在可以用它来进行推断。贝叶斯网络中有许多不同的算法来作为推断的的工具,在速度、复杂性、普遍性和精确性上有不同的表现。BNT因此提供了多种多样的不同的推断引擎。
enter_evidence,引擎可以处理一些经过特殊处理的证据。最后,当调用,marginal_nodes引擎可以执行一些特殊处理的查询。

 

 
最简单的推理方法是直接构建所有结点的联合分布,然后得到边缘概率。这已在global_joint_inf_engine中实现,但它仅适用于教学和调试。

 

Python贝叶斯文档分类模型

 

(1)收集数据:可以使用任何方法。本文使用RSS源
(2)准备数据:需要数值型或者布尔型数据
(3)分析数据:有大量特征时,绘制特征作用不大,此时使用直方图效果更好
(4)训练算法:计算不同的独立特征的条件概率
(5)测试算法:计算错误率
(6)使用算法:一个常见的朴素贝叶斯应用是文档分类。可以在任意的分类场景中使用朴素贝叶斯分类器,不一定非要是文本。
准备数据:从文本中构建词向量






以上是六句话,标记是0句子的表示正常句,标记是1句子的表示为粗口。我们通过分析每个句子中的每个词,在粗口句或是正常句出现的概率,可以找出那些词是粗口。
在bayes.py文件中添加如下代码:

 
训练算法:从词向量计算概率
1. # 朴素贝叶斯分类器训练函数 
 


 
测试算法:根据现实情况修改分类器
上一节中的trainNB0函数中修改几处:
1. # 朴素贝叶斯分类器训练函数 
22. # 朴素贝叶斯分类函数 
 


 
准备数据:文档词袋模型
词集模型(set-of-words model):每个词是否出现,每个词只能出现一次

1. # 朴素贝叶斯词袋模型 
 
示例:使用朴素贝叶斯过滤垃圾邮件


(1)收集数据:提供文本文件


(2)准备数据:将文本文件解析成词条向量


(3)分析数据:检查词条确保解析的正确性


(4)训练算法:使用我们之前建立的trainNB0()函数


(5)测试算法:使用classifyNB(),并且构建一个新的测试函数来计算文档集的错误率


(6)使用算法:构建一个完整的程序对一组文档进行分类,将错分的文档输出到屏幕上


使用正则表达式切分句子

 
测试算法:使用朴素贝叶斯进行交叉验证
1. # 该函数接受一个大写字符的字串,将其解析为字符串列表 
2. # 该函数去掉少于两个字符的字符串,并将所有字符串转换为小写 
8. # 完整的垃圾邮件测试函数 
13. # 导入并解析文本文件 
28. # 随机构建训练集 
 


 
因为这些电子邮件是随机选择的,所以每次输出的结果可能会不一样。

今天的推文就到这吧,我感冒发烧了,有点难受。各位晚安。

 

知识、能力、深度、专业
勤奋、天赋、耐得住寂寞
}

[版权声明] 本站所有资料由用户提供并上传,若内容存在侵权,请联系邮箱。资料中的图片、字体、音乐等需版权方额外授权,请谨慎使用。网站中党政主题相关内容(国旗、国徽、党徽)仅限个人学习分享使用,禁止广告使用和商用。

}

终于到函数了!因为Go汇编语言中,可以也建议通过Go语言来定义全局变量,那么剩下的也就是函数了。只有掌握了汇编函数的基本用法,才能真正算是Go汇编语言入门。本章将简单讨论Go汇编中函数的定义和用法。

函数标识符通过TEXT汇编指令定义,表示该行开始的指令定义在TEXT内存段。TEXT语句后的指令一般对应函数的实现,但是对于TEXT指令本身来说并不关心后面是否有指令。因此TEXTLABEL定义的符号是类似的,区别只是LABEL是用于跳转标号,但是本质上他们都是通过标识符映射一个内存地址。

函数的定义的语法如下:

函数的定义部分由5个部分组成:TEXT指令函数名可选的flags标志函数帧大小可选的函数参数大小

其中TEXT用于定义函数符号,函数名中当前包的路径可以省略。函数的名字后面是(SB),表示是函数名符号相对于SB伪寄存器的偏移量,二者组合在一起最终是绝对地址。作为全局的标识符的全局变量和全局函数的名字一般都是基于SB伪寄存器的相对地址。标志部分用于指示函数的一些特殊行为,标志在textlags.h文件中定义,常见的NOSPLIT主要用于指示叶子函数不进行栈分裂。framesize部分表示函数的局部变量需要多少栈空间,其中包含调用其它函数时准备调用参数的隐式栈空间。最后是可以省略的参数大小,之所以可以省略是因为编译器可以从Go语言的函数声明中推导出函数参数的大小。

我们首先从一个简单的Swap函数开始。Swap函数用于交互输入的两个参数的顺序,然后通过返回值返回交换了顺序的结果。如果用Go语言中声明Swap函数,大概这样的:

下面是main包中Swap函数在汇编中两种定义方式:

下图是Swap函数几种不同写法的对比关系图:

第一种是最完整的写法:函数名部分包含了当前包的路径,同时指明了函数的参数大小为32个字节(对应参数和返回值的4个int类型)。第二种写法则比较简洁,省略了当前包的路径和参数的大小。如果有NOSPLIT标注,会禁止汇编器为汇编函数插入栈分裂的代码。NOSPLIT对应Go语言中的//go:nosplit注释。

需要注意的是函数也没有类型,上面定义的Swap函数签名可以下面任意一种格式:

对于汇编函数来说,只要是函数的名字和参数大小一致就可以是相同的函数了。而且在Go汇编语言中,输入参数和返回值参数是没有任何的区别的。

对于函数来说,最重要的是函数对外提供的API约定,包含函数的名称、参数和返回值。当这些都确定之后,如何精确计算参数和返回值的大小是第一个需要解决的问题。

比如有一个Swap函数的签名如下:

对于这个函数,我们可以轻易看出它需要4个int类型的空间,参数和返回值的大小也就是32个字节:

那么如何在汇编中引用这4个参数呢?为此Go汇编中引入了一个FP伪寄存器,表示函数当前帧的地址,也就是第一个参数的地址。因此我们以通过+0(FP)+8(FP)+16(FP)+24(FP)来分别引用a、b、ret0和ret1四个参数。

但是在汇编代码中,我们并不能直接以+0(FP)的方式来使用参数。为了编写易于维护的汇编代码,Go汇编语言要求,任何通过FP伪寄存器访问的变量必和一个临时标识符前缀组合后才能有效,一般使用参数对应的变量名作为前缀

下图是Swap函数中参数和返回值在内存中的布局图:

下面的代码演示了如何在汇编函数中使用参数和返回值:

从代码可以看出a、b、ret0和ret1的内存地址是依次递增的,FP伪寄存器是第一个变量的开始地址

参数和返回值的内存布局

如果是参数和返回值类型比较复杂的情况该如何处理呢?下面我们再尝试一个更复杂的函数参数和返回值的计算。比如有以下一个函数:

函数的参数有不同的类型,而且返回值中含有更复杂的切片类型。我们该如何计算每个参数的位置和总的大小呢?

其实函数参数和返回值的大小以及对齐问题结构体的大小和成员对齐问题一致的,函数的第一个参数第一个返回值分别进行一次地址对齐。我们可以用诡代思路将全部的参数和返回值以同样的顺序分别放到两个结构体中,将FP伪寄存器作为唯一的一个指针参数,而每个成员的地址也就是对应原来参数的地址。

用这样的策略可以很容易计算前面的Foo函数的参数和返回值的地址和总大小。为了便于描述我们定义一个Foo_args_and_returns临时结构体类型用于诡代原始的参数和返回值:

然后将Foo原来的参数替换为结构体形式,并且只保留唯一的FP作为参数:

代码完全和Foo函数参数的方式类似。唯一的差异是每个函数的偏移量,通过unsafe.Offsetof函数自动计算生成。因为Go结构体中的每个成员已经满足了对齐要求,因此采用通用方式得到每个参数的偏移量也是满足对齐要求的。序言注意的是第一个返回值地址需要重新对齐机器字大小的倍数。

Foo函数的参数和返回值的大小和内存布局:

下面的代码演示了Foo汇编函数参数和返回值的定位:

其中a和b参数之间出现了一个字节的空洞,b和c之间出现了4个字节的空洞。出现空洞的原因是要保证每个参数变量地址都要对齐到相应的倍数。

从Go语言函数角度讲,局部变量是函数内明确定义的变量,同时也包含函数的参数和返回值变量。但是从Go汇编角度看,局部变量是指函数运行时,在当前函数栈帧所对应的内存内的变量,不包含函数的参数和返回值(因为访问方式有差异)。函数栈帧的空间主要由函数参数和返回值、局部变量和被调用其它函数的参数和返回值空间组成。为了便于理解,我们可以将汇编函数的局部变量类比为Go语言函数中显式定义的变量,不包含参数和返回值部分。

为了便于访问局部变量,Go汇编语言引入了伪SP寄存器,对应当前栈帧的底部因为在当前栈帧时栈的底部是固定不变的,因此局部变量的相对于伪SP的偏移量也就是固定的,这可以简化局部变量的维护工作。SP真伪寄存器的区分只有一个原则:如果使用SP时有一个临时标识符前缀就是伪SP,否则就是真SP寄存器。比如a(SP)和b+8(SP)有a和b临时前缀,这里都是伪SP,而前缀部分一般用于表示局部变量的名字。而(SP)和+8(SP)没有临时标识符作为前缀,它们都是真SP寄存器。

在X86平台,函数的调用栈是从高地址向低地址增长的,因此伪SP寄存器对应栈帧的底部其实是对应更大的地址。当前栈的顶部对应真实存在的SP寄存器,对应当前函数栈帧的栈顶,对应更小的地址。如果整个内存用Memory数组表示,那么Memory[0(SP):end-0(SP)]就是对应当前栈帧的切片,其中开始位置是真SP寄存器,结尾部分是伪SP寄存器。真SP寄存器一般用于表示调用其它函数时的参数和返回值,真SP寄存器对应内存较低的地址,所以被访问变量的偏移量是正数;而伪SP寄存器对应高地址,对应的局部变量的偏移量都是负数。

为了便于对比,我们将前面Foo函数的参数和返回值变量改成局部变量:

然后通过汇编语言重新实现Foo函数,并通过伪SP来定位局部变量:

Foo函数有3个局部变量,但是没有调用其它的函数,因为对齐和填充的问题导致函数的栈帧大小为32个字节。因为Foo函数没有参数和返回值,因此参数和返回值大小为0个字节,当然这个部分可以省略不写。而局部变量中先定义的变量c离伪SP寄存器对应的地址最近,最后定义的变量a离伪SP寄存器最远。有两个因素导致出现这种逆序的结果:一个从Go语言函数角度理解,先定义的c变量地址要比后定义的变量的地址更大;另一个是伪SP寄存器对应栈帧的底部,而X86中栈是从高向低生长的,所以最先定义有着更大地址的c变量离栈的底部伪SP更近。

我们同样可以通过结构体来模拟局部变量的布局:

我们将之前的三个局部变量挪到一个结构体中。然后构造一个SP变量对应伪SP寄存器,对应局部变量结构体的顶部。然后根据局部变量总大小和每个变量对应成员的偏移量计算相对于伪SP的距离,最终偏移量是一个负数。

通过这种方式可以处理复杂的局部变量的偏移,同时也能保证每个变量地址的对齐要求。当然,除了地址对齐外,局部变量的布局并没有顺序要求。对于汇编比较熟悉同学可以根据自己的习惯组织变量的布局。

下面是Foo函数的局部变量的大小和内存布局:

从图中可以看出Foo函数局部变量和前一个例子中参数和返回值的内存布局是完全一样的,这也是我们故意设计的结果。但是参数和返回值是通过伪FP寄存器定位的FP寄存器对应第一个参数的开始地址(第一个参数地址较低),因此每个变量的偏移量是正数。而局部变量是通过伪SP寄存器定位的而伪SP寄存器对应的是第一个局部变量的结束地址(第一个局部变量地址较大),因此每个局部变量的偏移量都是负数。

常见的用Go汇编实现的函数都是叶子函数,也就是被其它函数调用的函数,但是很少调用其它函数。这主要是因为叶子函数比较简单,可以简化汇编函数的编写;同时一般性能或特性的瓶颈也处于叶子函数。但是能够调用其它函数和能够被其它函数调用同样重要,否则Go汇编就不是一个完整的汇编语言。

在前文中我们已经学习了一些汇编实现的函数参数和返回值处理的规则。那么一个显然的问题是,汇编函数的参数是从哪里来的?答案同样明显,被调用函数的参数是由调用方准备的:调用方在栈上设置好空间和数据后调用函数,被调用方在返回前将返回值放在对应的位置,函数通过RET指令返回调用方函数之后,调用方再从返回值对应的栈内存位置取出结果。Go语言函数的调用参数和返回值均是通过栈传输的,这样做的优点是函数调用栈比较清晰,缺点是函数调用有一定的性能损耗(Go编译器是通过函数内联来缓解这个问题的影响)。

为了便于展示,我们先使用Go语言来构造三个逐级调用的函数:

其中main函数通过字面值常量直接调用printsum函数,printsum函数输出两个整数的和。而printsum函数内部又通过调用sum函数计算两个数的和,并最终调用打印函数进行输出。因为printsum既是被调用函数又是调用函数,所以它是我们要重点分析的函数。

下图展示了三个函数逐级调用时内存中函数参数和返回值的布局:

为了便于理解,我们对真实的内存布局进行了简化。要记住的是调用函数时,被调用函数的参数和返回值内存空间都必须由调用者提供。因此函数的局部变量和为调用其它函数准备的栈空间总和就确定了函数帧的大小。调用其它函数前调用方要选择保存相关寄存器到栈中,并在调用函数返回后选择要恢复的寄存器进行保存。最终通过CALL指令调用函数的过程和调用我们熟悉的调用println函数输出的过程类似。

Go语言中函数调用是一个复杂的问题,因为Go函数不仅仅要了解函数调用参数的布局,还会涉及到栈的跳转,栈上局部变量的生命周期管理。本节只是简单了解函数调用参数的布局规则,在后续的章节中会更详细的讨论函数的细节。

宏函数并不是Go汇编语言所定义,而是Go汇编引入的预处理特性自带的特性。

在C语言中我们可以通过带参数的宏定义一个交换2个数的宏函数:

我们可以用类似的方式定义一个交换两个寄存器的宏:

因为汇编语言中无法定义临时变量,我们增加一个参数用于临时寄存器。下面是通过SWAP宏函数交换AX和BX寄存器的值,然后返回结果:

因为预处理器可以通过条件编译针对不同的平台定义宏的实现,这样可以简化平台带来的差异。

在Go汇编语言中CALL指令用于调用函数,RET指令用于从调用函数返回。但是CALL和RET指令并没有处理函数调用时输入参数和返回值的问题。CALL指令类似PUSH IP和JMP somefunc两个指令的组合,首先将当前的IP指令寄存器的值压入栈中,然后通过JMP指令将要调用函数的地址写入到IP寄存器实现跳转。而RET指令则是和CALL相反的操作,基本和POP IP指令等价,也就是将执行CALL指令时保存在SP中的返回地址重新载入到IP寄存器,实现函数的返回。

和C语言函数不同,Go语言函数的参数和返回值完全通过栈传递。下面是Go函数调用时栈的布局图:

首先是调用函数前准备的输入参数和返回值空间。然后CALL指令将首先触发返回地址入栈操作。在进入到被调用函数内之后,汇编器自动插入了BP寄存器相关的指令,因此BP寄存器和返回地址是紧挨着的。再下面就是当前函数的局部变量的空间,包含再次调用其它函数需要准备的调用参数空间。被调用的函数执行RET返回指令时,先从栈恢复BP和SP寄存器,接着取出的返回地址跳转到对应的指令执行。

Go汇编语言其实是一种高级的汇编语言。在这里高级一词并没有任何褒义或贬义的色彩,而是要强调Go汇编代码和最终真实执行的代码并不完全等价。Go汇编语言中一个指令在最终的目标代码中可能会被编译为其它等价的机器指令。Go汇编实现的函数或调用函数的指令在最终代码中也会被插入额外的指令。要彻底理解Go汇编语言就需要彻底了解汇编器到底插入了哪些指令。

输出代码中我们删除了非指令的部分。为了便于讲述,我们将上述代码重新排版,并根据缩进表示相关的功能:

如果去掉NOSPILT标志,再重新查看生成的目标代码,会发现在函数的开头和结尾的地方又增加了新的指令。下面是经过缩进格式化的结果:

其中开头有三个新指令,MOVQ (TLS), CX用于加载g结构体指针,然后第二个指令CMPQ SP, 16(CX)SP栈指针和g结构体中stackguard0成员比较,如果比较的结果小于0则跳转到结尾的L_MORE_STK部分。当获取到更多栈空间之后,通过JMP L_BEGIN指令跳转到函数的开始位置重新进行栈空间的检测。

第一个成员是stack类型,表示当前栈的开始和结束地址。stack的定义如下:

以上是栈的扩容,但是栈的收缩是在何时处理的呢?我们知道Go运行时会定期进行垃圾回收操作,这其中包含栈的回收工作。如果栈使用到比例小于一定到阈值,则分配一个较小到栈空间,然后将栈上面到数据移动到新的栈中,栈移动的过程和栈扩容的过程类似。

比如以下代码可以打印程序的启动流程:

Go语言作为一门静态编译型语言,在执行时每个函数的地址都是固定的,函数的每条指令也是固定的。如果针对每个函数和函数的每个指令生成一个地址表格(也叫PC表格),那么在运行时我们就可以根据PC寄存器的值轻松查询到指令当时对应的函数和位置信息。而Go语言也是采用类似的策略,只不过地址表格经过裁剪,舍弃了不必要的信息。因为要在运行时获取任意一个地址的位置,必然是要有一个函数调用,因此我们只需要为函数的开始和结束位置,以及每个函数调用位置生成地址表格就可以了。同时地址是有大小顺序的,在排序后可以通过只记录增量来减少数据的大小;在查询时可以通过二分法加快查找的速度。

。PCDATA有个两个参数,第一个参数为表格的类型,第二个是表格的地址。在目前的实现中,有PCDATA_StackMapIndexPCDATA_InlTreeIndex两种表格类型。两种表格的数据是类似的,应该包含了代码所在的文件路径、行号和函数的信息,只不过PCDATA_InlTreeIndex用于内联函数的表格。

此外对于汇编函数中返回值包含指针的类型,在返回值指针被初始化之后需要执行一个GO_RESULTS_INITIALIZED指令:

GO_RESULTS_INITIALIZED记录的也是PC表格的信息,表示PC指针越过某个地址之后返回值才完成被初始化的状态。

Go语言二进制文件中除了有PC表格,还有FUNC表格用于记录函数的参数、局部变量的指针信息。FUNCDATA指令和PCDATA的格式类似:FUNCDATA tableid, tableoffset,第一个参数为表格的类型,第二个是表格的地址。目前的实现中定义了三种FUNC表格类型:FUNCDATA_ArgsPointerMaps表示函数参数的指针信息表,FUNCDATA_LocalsPointerMaps表示局部指针信息表,FUNCDATA_InlTree表示被内联展开的指针信息表。通过FUNC表格,Go语言的垃圾回收器可以跟踪全部指针的生命周期,同时根据指针指向的地址是否在被移动的栈范围来确定是否要进行指针移动。

在前面递归函数的例子中,我们遇到一个NO_LOCAL_POINTERS宏。它的定义如下:

PCDATAFUNCDATA的数据一般是由编译器自动生成的,手工编写并不现实。如果函数已经有Go语言声明,那么编译器可以自动输出参数和返回值的指针表格。同时所有的函数调用一般是对应CALL指令,编译器也是可以辅助生成PCDATA表格的。编译器唯一无法自动生成是函数局部变量的表格,因此我们一般要在汇编函数的局部变量中谨慎使用指针类型。

对于PCDATA和FUNCDATA细节感兴趣的同学可以尝试从debug/gosym包入手,参考包的实现和测试代码。

Go语言中方法函数和全局函数非常相似,比如有以下的方法:

其中MyInt类型的Twice方法和MyInt_Twice函数的类型是完全一样的,只不过Twice在目标文件中被修饰为main.MyInt.Twice名称。我们可以用汇编实现该方法函数:

不过这只是接收非指针类型的方法函数。现在增加一个接收参数是指针类型的Ptr方法,函数返回传入的指针:

在目标文件中,Ptr方法名被修饰为main.(*MyInt).Ptr,也就是对应汇编中的·(*MyInt)·Ptr。不过在Go汇编语言中,星号和小括弧都无法用作函数名字,也就是无法用汇编直接实现接收参数是指针类型的方法。

在最终的目标文件中的标识符名字中还有很多Go汇编语言不支持的特殊符号(比如type.string."hello"中的双引号),这导致了无法通过手写的汇编代码实现全部的特性。或许是Go语言官方故意限制了汇编语言的特性。

递归函数: 1到n求和

递归函数是比较特殊的函数,递归函数通过调用自身并且在栈上保存状态,这可以简化很多问题的处理。Go语言中递归函数的强大之处是不用担心爆栈问题,因为栈可以根据需要进行扩容和收缩。

首先通过Go递归函数实现一个1到n的求和函数:

然后通过if/goto重构上面的递归函数,以便于转义为汇编版本:

在改写之后,递归调用的参数需要引入局部变量,保存中间结果也需要引入局部变量。而通过栈来保存中间的调用状态正是递归函数的核心。因为输入参数也在栈上,所以我们可以通过输入参数来保存少量的状态。同时我们模拟定义了AX和BX寄存器,寄存器在使用前需要初始化,并且在函数调用后也需要重新初始化。

下面继续改造为汇编语言版本:

在汇编版本函数中并没有定义局部变量,只有用于调用自身的临时栈空间。因为函数本身的参数和返回值有16个字节,因此栈帧的大小也为16字节。L_STEP_TO_END标号部分用于处理递归调用,是函数比较复杂的部分。L_END用于处理递归终结的部分。

调用sum函数的参数在0(SP)位置,调用结束后的返回值在8(SP)位置。在函数调用之后要需要重新为需要的寄存器注入值,因为被调用的函数内部很可能会破坏了寄存器的状态。同时调用函数的参数值也是不可信任的,输入参数值也可能在被调用函数内部被修改了。

总得来说用汇编实现递归函数和普通函数并没有什么区别,当然是在没有考虑爆栈的前提下。我们的函数应该可以对较小的n进行求和,但是当n大到一定程度,也就是栈达到一定的深度,必然会出现爆栈的问题。爆栈是C语言的特性,不应该在哪怕是Go汇编语言中出现。

Go语言的编译器在生成函数的机器代码时,会在开头插入一小段代码。因为sum函数也需要深度递归调用,因此我们删除了NOSPLIT标志,让汇编器为我们自动生成一个栈扩容的代码:

除了去掉了NOSPLIT标志,我们还在函数开头增加了一个NO_LOCAL_POINTERS语句,该语句表示函数没有局部指针变量。栈的扩容必然要涉及函数参数和局部编指针的调整,如果缺少局部指针信息将导致扩容工作无法进行。不仅仅是栈的扩容需要函数的参数和局部指针标记表格,在GC进行垃圾回收时也将需要。函数的参数和返回值的指针状态可以通过在Go语言中的函数声明中获取,函数的局部变量则需要手工指定。因为手工指定指针表格是一个非常繁琐的工作,因此一般要避免在手写汇编中出现局部指针。

喜欢深究的读者可能会有一个问题:如果进行垃圾回收或栈调整时,寄存器中的指针是如何维护的?前文说过,Go语言的函数调用是通过栈进行传递参数的,并没有使用寄存器传递参数。同时函数调用之后所有的寄存器视为失效。因此在调整和维护指针时,只需要扫描内存中的指针数据,寄存器中的数据在垃圾回收器函数返回后都需要重新加载,因此寄存器是不需要扫描的。

闭包函数是最强大的函数,因为闭包函数可以捕获外层局部作用域的局部变量,因此闭包函数本身就具有了状态。从理论上来说,全局的函数也是闭包函数的子集,只不过全局函数并没有捕获外层变量而已。

为了理解闭包函数如何工作,我们先构造如下的例子:

其中NewTwiceFunClosure函数返回一个闭包函数对象,返回的闭包函数对象捕获了外层的x参数。返回的闭包函数对象在执行时,每次将捕获的外层变量乘以2之后再返回。在main函数中,首先以1作为参数调用NewTwiceFunClosure函数构造一个闭包函数,返回的闭包函数保存在fnTwice闭包函数类型的变量中。然后每次调用fnTwice闭包函数将返回翻倍后的结果,也就是:2,4,8。

上述的代码,从Go语言层面是非常容易理解的。但是闭包函数在汇编语言层面是如何工作的呢?下面我们尝试手工构造闭包函数来展示闭包的工作原理。首先是构造```FunTwiceClosure````结构体类型,用来表示闭包对象:

FunTwiceClosure结构体包含两个成员,第一个成员F表示闭包函数的函数指令的地址,第二个成员X表示闭包捕获的外部变量。如果闭包函数捕获了多个外部变量,那么FunTwiceClosure结构体也要做相应的调整。然后构造FunTwiceClosure结构体对象,其实也就是闭包函数对象。其中asmFunTwiceClosureAddr函数用于辅助获取闭包函数的函数指令的地址,采用汇编语言实现。最后通过ptrToFunc辅助函数将结构体指针转为闭包函数对象返回,该函数也是通过汇编语言实现。

汇编语言实现了以下三个辅助函数:

然后用Go汇编语言实现以上三个辅助函数:

其中·ptrToFunc·asmFunTwiceClosureAddr函数的实现比较简单,我们不再详细描述。最重要的是·asmFunTwiceClosureBody函数的实现:它有一个NEEDCTXT标志。采用NEEDCTXT标志定义的汇编函数表示需要一个上下文环境,在AMD64环境下是通过DX寄存器来传递这个上下文环境指针,也就是对应FunTwiceClosure结构体的指针。函数首先从FunTwiceClosure结构体对象取出之前捕获的X,将X乘以2之后写回内存,最后返回修改之后的X的值。

如果是在汇编语言中调用闭包函数,也需要遵循同样的流程:首先为构造闭包对象,其中保存捕获的外层变量;在调用闭包函数时首先要拿到闭包对象,用闭包对象初始化DX,然后从闭包对象中取出函数地址并用通过CALL指令调用。

}

我要回帖

更多关于 函数和映射的区别图解 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信