热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

vbs比较两个数组里的数的大小_Golang数据结构详解之数组

上一章中对于编译原理的说明如下:1概述2词法和语法分析3类型检查4中间代码生成5机器码生成接下来我们来对golang的数据结构进行说明,主要内容有&#x
c7e50d70c63c4c44caedf86227f156b9.png

上一章中对于编译原理的说明如下:

  • 1 概述
  • 2 词法和语法分析
  • 3 类型检查
  • 4 中间代码生成
  • 5 机器码生成

接下来我们来对golang的数据结构进行说明,主要内容有:

  • 1 数组
  • 2 切片
  • 3 哈希表
  • 4 字符串

数组和切片是 Go 语言中常见的数据结构,很多刚刚使用 Go 的开发者往往会混淆这两个概念,数组作为最常见的集合在编程语言中是非常重要的,除了数组之外,Go 语言引入了另一个概念 — 切片,切片与数组有一些类似,但是它们的不同之处导致使用上会产生巨大的差别。我们在这一节中会从 Go 语言的编译期间运行时来介绍数组的底层实现原理,其中会包括数组的初始化、访问和赋值几种常见操作。

1. 概述

数组是由相同类型元素的集合组成的数据结构,计算机会为数组分配一块连续的内存来保存其中的元素,我们可以利用数组中元素的索引快速访问元素对应的存储地址,常见的数组大多都是一维的线性数组,而多维数组在数值和图形计算领域却有比较常见的应用1。

a2ba13185550705d7a8413bda31213e9.png
图 - 多维数组

数组作为一种基本的数据类型,我们通常都会从两个维度描述数组,我们首先需要描述数组中存储的元素类型,还需要描述数组最大能够存储的元素个数,在 Go 语言中我们往往会使用如下所示的方式来表示数组类型:

[10]int
[200]interface{}

与很多语言不同,Go 语言中数组在初始化之后大小就无法改变,存储元素类型相同、但是大小不同的数组类型在 Go 语言看来也是完全不同的,只有两个条件都相同才是同一个类型。

func NewArray(elem *Type, bound int64) *Type {if bound <0 {Fatalf("NewArray: invalid bound %v", bound)}t :&#61; New(TARRAY)t.Extra &#61; &Array{Elem: elem, Bound: bound}t.SetNotInHeap(elem.NotInHeap())return t
}

编译期间的数组类型是由上述的 cmd/compile/internal/types.NewArray 函数生成的&#xff0c;类型 Array 包含两个字段&#xff0c;一个是元素类型 Elem&#xff0c;另一个是数组的大小 Bound&#xff0c;这两个字段共同构成了数组类型&#xff0c;而当前数组是否应该在堆栈中初始化也在编译期就确定了。

2. 初始化

Go 语言中的数组有两种不同的创建方式&#xff0c;一种是显式的指定数组的大小&#xff0c;另一种是使用 [...]T 声明数组&#xff0c;Go 语言会在编译期间通过源代码对数组的大小进行推断&#xff1a;

arr1 :&#61; [3]int{1, 2, 3}
arr2 :&#61; [...]int{1, 2, 3}

上述两种声明方式在运行期间得到的结果是完全相同的&#xff0c;后一种声明方式在编译期间就会被『转换』成为前一种&#xff0c;这也就是编译器对数组大小的推导&#xff0c;下面我们来介绍编译器的推导过程。

上限推导

两种不同的声明方式会导致编译器做出完全不同的处理&#xff0c;如果我们使用第一种方式 [10]T&#xff0c;那么变量的类型在编译进行到类型检查阶段就会被提取出来&#xff0c;随后会使用 cmd/compile/internal/types.NewArray 函数创建包含数组大小的 Array 类型。

当我们使用 [...]T 的方式声明数组时&#xff0c;虽然在这一步也会创建一个 Array 类型 Array{Elem: elem, Bound: -1}&#xff0c;但是其中的数组大小上限会是 -1&#xff0c;这里的 -1 只是一个占位符&#xff0c;编译器会在后面的 cmd/compile/internal/gc.typecheckcomplit 函数中对该数组的大小进行推导&#xff1a;

func typecheckcomplit(n *Node) (res *Node) {...switch t.Etype {case TARRAY, TSLICE:var length, i int64nl :&#61; n.List.Slice()for i2, l :&#61; range nl {i&#43;&#43;if i > length {length &#61; i}}if t.IsDDDArray() {t.SetNumElem(length)}}
}

这个删减后的 cmd/compile/internal/gc.typecheckcomplit 函数通过遍历元素的方式来计算数组中元素的数量。上述代码中的 DDDArray 指的就是使用 [...]T 声明的数组&#xff0c;因为声明这种数组时需要使用三个点&#xff08;Dot&#xff09;&#xff0c;所以在编译器中就被称作 DDDArray

所以我们可以看出 [...]T{1, 2, 3}[3]T{1, 2, 3} 在运行时是完全等价的&#xff0c;[...]T 这种初始化方式也只是 Go 语言为我们提供的一种语法糖&#xff0c;当我们不想计算数组中的元素个数时就可以通过这种方法较少一些工作。

语句转换

对于一个由字面量组成的数组&#xff0c;根据数组元素数量的不同&#xff0c;编译器会在负责初始化字面量的 cmd/compile/internal/gc.anylit 函数中做两种不同的优化&#xff1a;

  1. 当元素数量小于或者等于 4 个时&#xff0c;会直接将数组中的元素放置在栈上&#xff1b;
  2. 当元素数量大于 4 个时&#xff0c;会将数组中的元素放置到静态区并在运行时取出&#xff1b;

func anylit(n *Node, var_ *Node, init *Nodes) {t :&#61; n.Typeswitch n.Op {case OSTRUCTLIT, OARRAYLIT:if n.List.Len() > 4 {...}fixedlit(inInitFunction, initKindLocalCode, n, var_, init)...}
}

当数组的元素小于或者等于四个时&#xff0c;cmd/compile/internal/gc.fixedlit 会负责在函数编译之前将 [3]{1, 2, 3} 转换成更加原始的语句&#xff1a;

func fixedlit(ctxt initContext, kind initKind, n *Node, var_ *Node, init *Nodes) {var splitnode func(*Node) (a *Node, value *Node)...for _, r :&#61; range n.List.Slice() {a, value :&#61; splitnode(r)a &#61; nod(OAS, a, value)a &#61; typecheck(a, ctxStmt)switch kind {case initKindStatic:genAsStatic(a)case initKindLocalCode:a &#61; orderStmtInPlace(a, map[string][]*Node{})a &#61; walkstmt(a)init.Append(a)}}
}

当数组中元素的个数小于或者等于四个时&#xff0c;cmd/compile/internal/gc.fixedlit 函数接受的 kindinitKindLocalCode&#xff0c;上述代码会将原有的初始化语句 [3]int{1, 2, 3} 拆分成一个声明变量的表达式和几个赋值表达式&#xff0c;这些表达式会完成对数组的初始化&#xff1a;

var arr [3]int
arr[0] &#61; 1
arr[1] &#61; 2
arr[2] &#61; 3

但是如果当前数组的元素大于四个&#xff0c;anylit 方法会先获取一个唯一的 staticname&#xff0c;然后调用 cmd/compile/internal/gc.fixedlit 函数在静态存储区初始化数组中的元素并将临时变量赋值给当前的数组&#xff1a;

func anylit(n *Node, var_ *Node, init *Nodes) {t :&#61; n.Typeswitch n.Op {case OSTRUCTLIT, OARRAYLIT:if n.List.Len() > 4 {vstat :&#61; staticname(t)vstat.Name.SetReadonly(true)fixedlit(inNonInitFunction, initKindStatic, n, vstat, init)a :&#61; nod(OAS, var_, vstat)a &#61; typecheck(a, ctxStmt)a &#61; walkexpr(a, init)init.Append(a)break}...}
}

假设我们在代码中初始化 [5]int{1, 2, 3, 4, 5} 数组&#xff0c;那么我们可以将上述过程理解成以下的伪代码&#xff1a;

var arr [5]int
statictmp_0[0] &#61; 1
statictmp_0[1] &#61; 2
statictmp_0[2] &#61; 3
statictmp_0[3] &#61; 4
statictmp_0[4] &#61; 5
arr &#61; statictmp_0

总结起来&#xff0c;在不考虑逃逸分析的情况下&#xff0c;如果数组中元素的个数小于或者等于 4 个&#xff0c;那么所有的变量会直接在栈上初始化&#xff0c;如果数组元素大于 4 个&#xff0c;变量就会在静态存储区初始化然后拷贝到栈上&#xff0c;这些转换后的代码才会继续进入中间代码生成和机器码生成两个阶段&#xff0c;最后生成可以执行的二进制文件。

3. 访问和赋值

无论是在栈上还是静态存储区&#xff0c;数组在内存中其实就是一连串的内存空间&#xff0c;表示数组的方法就是一个指向数组开头的指针、数组中元素的数量以及数组中元素类型占的空间大小&#xff0c;如果我们不知道数组中元素的数量&#xff0c;访问时就可能发生越界&#xff0c;而如果不知道数组中元素类型的大小&#xff0c;就没有办法知道应该一次取出多少字节的数据&#xff0c;如果没有这些信息&#xff0c;我们就无法知道这片连续的内存空间到底存储了什么数据&#xff1a;

f2628229dd5954b5ec6d603625ede549.png
图 - 数组的内存空间

数组访问越界是非常严重的错误&#xff0c;Go 语言中对越界的判断是可以在编译期间由静态类型检查完成的&#xff0c;cmd/compile/internal/gc.typecheck1 函数会对访问数组的索引进行验证&#xff1a;

func typecheck1(n *Node, top int) (res *Node) {switch n.Op {case OINDEX:ok |&#61; ctxExprl :&#61; n.Left // arrayr :&#61; n.Right // indexswitch n.Left.Type.Etype {case TSTRING, TARRAY, TSLICE:...if n.Right.Type !&#61; nil && !n.Right.Type.IsInteger() {yyerror("non-integer array index %v", n.Right)break}if !n.Bounded() && Isconst(n.Right, CTINT) {x :&#61; n.Right.Int64()if x <0 {yyerror("invalid array index %v (index must be non-negative)", n.Right)} else if n.Left.Type.IsArray() && x >&#61; n.Left.Type.NumElem() {yyerror("invalid array index %v (out of bounds for %d-element array)", n.Right, n.Left.Type.NumElem())}}}...}
}

  1. 访问数组的索引是非整数时会直接报错 —— non-integer array index %v&#xff1b;
  2. 访问数组的索引是负数时会直接报错 —— "invalid array index %v (index must be non-negative)"&#xff1b;
  3. 访问数组的索引越界时会直接报错 —— "invalid array index %v (out of bounds for %d-element array)"&#xff1b;

数组和字符串的一些简单越界错误都会在编译期间发现&#xff0c;比如我们直接使用整数或者常量访问数组&#xff0c;但是如果使用变量去访问数组或者字符串时&#xff0c;编译器就无法发现对应的错误了&#xff0c;这时就需要 Go 语言运行时发挥作用了&#xff1a;

arr[4]: invalid array index 4 (out of bounds for 3-element array)
arr[i]: panic: runtime error: index out of range [4] with length 3

Go 语言运行时在发现数组、切片和字符串的越界操作会由运行时的 panicIndexruntime.goPanicIndex 函数触发程序的运行时错误并导致崩溃退出&#xff1a;

TEXT runtime·panicIndex(SB),NOSPLIT,$0-8MOVL AX, x&#43;0(FP)MOVL CX, y&#43;4(FP)JMP runtime·goPanicIndex(SB)func goPanicIndex(x int, y int) {panicCheck1(getcallerpc(), "index out of range")panic(boundsError{x: int64(x), signed: true, y: y, code: boundsIndex})
}

当数组的访问操作 OINDEX 成功通过编译器的检查之后&#xff0c;会被转换成几个 SSA 指令&#xff0c;假设我们有如下所示的 Go 语言代码&#xff0c;通过如下的方式进行编译会得到 ssa.html 文件&#xff1a;

package checkfunc outOfRange() int {arr :&#61; [3]int{1, 2, 3}i :&#61; 4elem :&#61; arr[i]return elem
}$ GOSSAFUNC&#61;outOfRange go build array.go
dumped SSA to ./ssa.html

start 阶段生成的 SSA 代码就是优化之前的第一版中间代码&#xff0c;下面展示的部分就是 elem :&#61; arr[i] 对应的中间代码&#xff0c;在这段中间代码中我们发现 Go 语言为数组的访问操作生成了判断数组上限的指令 IsInBounds 以及当条件不满足时触发程序崩溃的 PanicBounds 指令&#xff1a;

b1:...v22 (6) &#61; LocalAddr <*[3]int> {arr} v2 v20v23 (6) &#61; IsInBounds v21 v11
If v23 → b2 b3 (likely) (6)b2: ← b1-v26 (6) &#61; PtrIndex <*int> v22 v21v27 (6) &#61; Copy v20v28 (6) &#61; Load v26 v27 (elem[int])...
Ret v30 (&#43;7)b3: ← b1-v24 (6) &#61; Copy v20v25 (6) &#61; PanicBounds [0] v21 v11 v24
Exit v25 (6)

PanicBounds 指令最终会被转换成上面提到的 panicIndex 函数&#xff0c;当数组下标没有越界时&#xff0c;编译器会先获取数组的内存地址和访问的下标&#xff0c;然后利用 PtrIndex 计算出目标元素的地址&#xff0c;再使用 Load 操作将指针中的元素加载到内存中。

当然只有当编译器无法对数组下标是否越界无法做出判断时才会加入 PanicBounds 指令交给运行时进行判断&#xff0c;在使用字面量整数访问数组下标时就会生成非常简单的中间代码&#xff0c;当我们将上述代码中的 arr[i] 改成 arr[2] 时&#xff0c;就会得到如下所示的代码&#xff1a;

b1:...v21 (5) &#61; LocalAddr <*[3]int> {arr} v2 v20v22 (5) &#61; PtrIndex <*int> v21 v14v23 (5) &#61; Load v22 v20 (elem[int])...

Go 语言对于数组的访问还是有着比较多的检查的&#xff0c;它不仅会在编译期间提前发现一些简单的越界错误并插入用于检测数组上限的函数调用&#xff0c;而在运行期间这些插入的函数会负责保证不会发生越界错误。

数组的赋值和更新操作 a[i] &#61; 2 也会生成 SSA 生成期间计算出数组当前元素的内存地址&#xff0c;然后修改当前内存地址的内容&#xff0c;这些赋值语句会被转换成如下所示的 SSA 操作&#xff1a;

b1:...v21 (5) &#61; LocalAddr <*[3]int> {arr} v2 v19v22 (5) &#61; PtrIndex <*int> v21 v13v23 (5) &#61; Store {int} v22 v20 v19...

赋值的过程中会先确定目标数组的地址&#xff0c;再通过 PtrIndex 获取目标元素的地址&#xff0c;最后使用 Store 指令将数据存入地址中&#xff0c;从上面的这些 SSA 代码中我们可以看出无论是数组的寻址还是赋值都是在编译阶段完成的&#xff0c;没有运行时的参与。

4. 小结

数组是 Go 语言中重要的数据结构&#xff0c;了解它的实现能够帮助我们更好地理解这门语言&#xff0c;通过对其实现的分析&#xff0c;我们知道了对数组的访问和赋值需要同时依赖编译器和运行时&#xff0c;它的大多数操作在编译期间都会转换成对内存的直接读写&#xff0c;在中间代码生成期间&#xff0c;编译器还会插入运行时方法 panicIndex 调用防止发生越界错误。

全套教程点击下方链接直达&#xff1a;

IT实战&#xff1a;Go语言设计与实现自学教程​zhuanlan.zhihu.com
72641ffc4f10c2395887859dea35a061.png



推荐阅读
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 前景:当UI一个查询条件为多项选择,或录入多个条件的时候,比如查询所有名称里面包含以下动态条件,需要模糊查询里面每一项时比如是这样一个数组条件:newstring[]{兴业银行, ... [详细]
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • PriorityQueue源码分析
     publicbooleanhasNext(){returncursor&amp;amp;lt;size||(forgetMeNot!null&amp;amp;am ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • 本文介绍了Perl的测试框架Test::Base,它是一个数据驱动的测试框架,可以自动进行单元测试,省去手工编写测试程序的麻烦。与Test::More完全兼容,使用方法简单。以plural函数为例,展示了Test::Base的使用方法。 ... [详细]
  • 本文探讨了C语言中指针的应用与价值,指针在C语言中具有灵活性和可变性,通过指针可以操作系统内存和控制外部I/O端口。文章介绍了指针变量和指针的指向变量的含义和用法,以及判断变量数据类型和指向变量或成员变量的类型的方法。还讨论了指针访问数组元素和下标法数组元素的等价关系,以及指针作为函数参数可以改变主调函数变量的值的特点。此外,文章还提到了指针在动态存储分配、链表创建和相关操作中的应用,以及类成员指针与外部变量的区分方法。通过本文的阐述,读者可以更好地理解和应用C语言中的指针。 ... [详细]
  • 本文讨论了如何使用IF函数从基于有限输入列表的有限输出列表中获取输出,并提出了是否有更快/更有效的执行代码的方法。作者希望了解是否有办法缩短代码,并从自我开发的角度来看是否有更好的方法。提供的代码可以按原样工作,但作者想知道是否有更好的方法来执行这样的任务。 ... [详细]
  • Go语言实现堆排序的详细教程
    本文主要介绍了Go语言实现堆排序的详细教程,包括大根堆的定义和完全二叉树的概念。通过图解和算法描述,详细介绍了堆排序的实现过程。堆排序是一种效率很高的排序算法,时间复杂度为O(nlgn)。阅读本文大约需要15分钟。 ... [详细]
  • JVM:33 如何查看JVM的Full GC日志
    1.示例代码packagecom.webcode;publicclassDemo4{publicstaticvoidmain(String[]args){byte[]arr ... [详细]
  • 初识java关于JDK、JRE、JVM 了解一下 ... [详细]
  • Python中程序员的面试题有哪些
    小编给大家分享一下Python中程序员的面试题有哪些,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有 ... [详细]
  • Apple iPad:过渡设备还是平板电脑?
    I’vebeenagonizingoverwhethertopostaniPadarticle.Applecertainlydon’tneedmorepublicityandthe ... [详细]
  • 开发笔记:快速排序和堆排序
    本文由编程笔记#小编为大家整理,主要介绍了快速排序和堆排序相关的知识,希望对你有一定的参考价值。快速排序思想:在partition中,首先以最右边的值作为划分值x,分别维护小于 ... [详细]
  • 尾部|柜台_Java并发线程池篇附场景分析
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了Java并发-线程池篇-附场景分析相关的知识,希望对你有一定的参考价值。作者:汤圆个人博客 ... [详细]
author-avatar
花花花
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有