Go设计与实现--数组与切片

news2024/12/23 14:51:45

数组

初始化

Go语言数组的初始化是在编译期就已经执行好了。这个是初始化的代码:

// NewArray returns a new fixed-length array Type.
func NewArray(elem *Type, bound int64) *Type {
   if bound < 0 {
      base.Fatalf("NewArray: invalid bound %v", bound)
   }
    // 根据TARRAY这个标识做响应的初始化
   t := newType(TARRAY)
    // 初始化元素类型Elem和数组大小Bound
   t.extra = &Array{Elem: elem, Bound: bound}
    // 是否应该初始化在堆里面
   t.SetNotInHeap(elem.NotInHeap())
    // 是否有参数
   if elem.HasTParam() {
      t.SetHasTParam(true)
   }
    // 笔者目前还不清楚这个Shape的含义
   if elem.HasShape() {
      t.SetHasShape(true)
   }
   return t
}

首先解释一下这个函数的返回值,Type是一个什么东西。

这个是Go中对于Type结构体的定义:

// A Type represents a Go type.
//
// There may be multiple unnamed types with identical structure. However, there must
// be a unique Type object for each unique named (defined) type. After noding, a
// package-level type can be looked up by building its unique symbol sym (sym =
// package.Lookup(name)) and checking sym.Def. If sym.Def is non-nil, the type
// already exists at package scope and is available at sym.Def.(*ir.Name).Type().
// Local types (which may have the same name as a package-level type) are
// distinguished by the value of vargen.
type Type struct {
   // extra contains extra etype-specific fields.
   // As an optimization, those etype-specific structs which contain exactly
   // one pointer-shaped field are stored as values rather than pointers when possible.
   //
   // TMAP: *Map
   // TFORW: *Forward
   // TFUNC: *Func
   // TSTRUCT: *Struct
   // TINTER: *Interface
   // TFUNCARGS: FuncArgs
   // TCHANARGS: ChanArgs
   // TCHAN: *Chan
   // TPTR: Ptr
   // TARRAY: *Array
   // TSLICE: Slice
   // TSSA: string
   // TTYPEPARAM:  *Typeparam
   // TUNION: *Union
   extra interface{}

   // width is the width of this Type in bytes.
   width int64 // valid if Align > 0

   // list of base methods (excluding embedding)
   methods Fields
   // list of all methods (including embedding)
   allMethods Fields

   // canonical OTYPE node for a named type (should be an ir.Name node with same sym)
   obj Object
   // the underlying type (type literal or predeclared type) for a defined type
   underlying *Type

   // Cache of composite types, with this type being the element type.
   cache struct {
      ptr   *Type // *T, or nil
      slice *Type // []T, or nil
   }

   vargen int32 // unique name for OTYPE/ONAME

   kind  Kind  // kind of type
   align uint8 // the required alignment of this type, in bytes (0 means Width and Align have not yet been computed)

   flags bitset8

   // For defined (named) generic types, a pointer to the list of type params
   // (in order) of this type that need to be instantiated. For instantiated
   // generic types, this is the targs used to instantiate them. These targs
   // may be typeparams (for re-instantiated types such as Value[T2]) or
   // concrete types (for fully instantiated types such as Value[int]).
   // rparams is only set for named types that are generic or are fully
   // instantiated from a generic type, and is otherwise set to nil.
   // TODO(danscales): choose a better name.
   rparams *[]*Type

   // For an instantiated generic type, the base generic type.
   // This backpointer is useful, because the base type is the type that has
   // the method bodies.
   origType *Type
}

它表示的是一个类型。其实不光是数组,切片,哈希表等在初始化的时候同样会返回这个Type结构体的指针。

func NewSlice(elem *Type) *Type
func NewChan(elem *Type, dir ChanDir) *Type
func NewTuple(t1, t2 *Type) *Type 
func NewMap(k, v *Type) *Type
。。。。

来看看NewType的源代码:

// New returns a new Type of the specified kind.
func newType(et Kind) *Type {
   t := &Type{
      kind:  et,
      width: BADWIDTH,
   }
   t.underlying = t
   // TODO(josharian): lazily initialize some of these?
   switch t.kind {
   case TMAP:
      t.extra = new(Map)
   case TFORW:
      t.extra = new(Forward)
   case TFUNC:
      t.extra = new(Func)
   case TSTRUCT:
      t.extra = new(Struct)
   case TINTER:
      t.extra = new(Interface)
   case TPTR:
      t.extra = Ptr{}
   case TCHANARGS:
      t.extra = ChanArgs{}
   case TFUNCARGS:
      t.extra = FuncArgs{}
   case TCHAN:
      t.extra = new(Chan)
   case TTUPLE:
      t.extra = new(Tuple)
   case TRESULTS:
      t.extra = new(Results)
   case TTYPEPARAM:
      t.extra = new(Typeparam)
   case TUNION:
      t.extra = new(Union)
   }
   return t
}

以上是Go初始化最本质的代码。

Go数组的初始化的时候有两种创建方式:

arr1 := [3]int {1, 2, 3} 
arr2 := [...]int {1, 2, 3}
  • 第一种创建方式是显示的指定的数组的大小
  • 第二种会在编译期间通过源代码推导数组的大小

这两种在运行时就已经完全一样了,只是Go给我们的语法糖而已。

因此我们现在讲一下推导的过程

上限推导

会调用函数使用遍历的方式来计算数组中元素的数量。

语句转换

对于由字面量组成的数组,根据数组元素数量的不同,编译器会在负责初始化字面量的函数中做两个优化:

  • 当元素小于等于4个的时候,会直接把数组中的元素放在栈上,并且在编译之前把其转换成更原始的语句。

    例如:

    arr := [3]int {1, 2, 3}
    // 会被转换成
    var arr [3]int 
    arr[0] = 1
    arr[1] = 2
    arr[2] = 3
    
  • 多于4个的时候,会把数组中的元素放置到静态区并在运行时取出。函数在静态存储区初始化数组中的元素,并将临时变量赋值给数组。

    例如:

    arr := [5]int {1, 2, 3, 4, 5}
    var arr [5]int
    statictmp_0[0] = 1
    statictmp_0[1] = 2
    statictmp_0[2] = 3
    statictmp_0[3] = 4
    statictmp_0[4] = 5
    arr = statictmp_0
    

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

访问和赋值

越界问题

  • 如果直接使用整数或者常量访问,在编译期间会通过静态类型检查大致的检查越界问题。

  • 如果使用变量访问数组或者字符串,编译期间无法发现错误,需要在runtime(运行时)发现错误并进行阻碍。具体过程为:

    1. 当数组的访问操作OINDEX成功通过编译器检查之后,会被转换成几个SSA指令。例如下面的这个代码生成的SSA:

      func outOfRange() int {
      	arr := [3]int{1, 2, 3}
      	i := 4
      	elem := arr[i]
      	return elem
      }
      
    2. 我们这里展示elem := arr[i]生成的SSA代码:

      b1:
          ...
          v22 (6) = LocalAddr <*[3]int> {arr} v2 v20
          v23 (6) = IsInBounds <bool> v21 v11
      If v23 → b2 b3 (likely) (6)
      
      b2: ← b1-
          v26 (6) = PtrIndex <*int> v22 v21
          v27 (6) = Copy <mem> v20
          v28 (6) = Load <int> v26 v27 (elem[int])
          ...
      Ret v30 (+7)
      
      b3: ← b1-
          v24 (6) = Copy <mem> v20
          v25 (6) = PanicBounds <mem> [0] v21 v11 v24
      Exit v25 (6)
      

      可以看到Go为访问操作生成了两个指令:

      IsInBounds:判断数组上限的指令

      PanicBounds:条件不满足时触发程序崩溃的指令

    3. 其中PanicBounds指令会转换成runtime.panicIndex函数。

      TEXT runtime·panicIndex(SB),NOSPLIT,$0-8
      	MOVL	AX, x+0(FP)
      	MOVL	CX, y+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})
      }
      

      如果越界了直接寄。如果没有越界的话编译器会获取数组的内存地址和访问的下标,利用PtrIndex计算出目标元素的地址,然后只用Load操作把指针中的元素加载到内存中。

      如下:

      b1:
          ...
          v21 (5) = LocalAddr <*[3]int> {arr} v2 v20
          v22 (5) = PtrIndex <*int> v21 v14
          v23 (5) = Load <int> v22 v20 (elem[int])
          ...
      
  • 对于赋值和更新操作a[i] = 2也会生成SSA,如下:

    b1:
        ...
        v21 (5) = LocalAddr <*[3]int> {arr} v2 v19
        v22 (5) = PtrIndex <*int> v21 v13
        v23 (5) = Store <mem> {int} v22 v20 v19
        ...
    

    赋值过程中先确定目标数组的地址,再通过PtrIndex获取目标元素的地址,最后使用Store指令将数据存入地址中。可以看到全程在编译阶段完成,没有runtime参与。

切片

数据结构

type sliceHeader struct {
   Data unsafe.Pointer // 指向数组的指针
   Len  int // 当前切片的长度
   Cap  int // 当前切片的容量
}

这个结构体是一个抽象层,源数组不管怎么发生变化,上层都认为切片没有变化,它不需要关心数组的变化。
请添加图片描述

初始化

  • 通过下标初始化
  • 使用字面量
  • make

使用下标

最原始的方式,最接近汇编。编译器会把arr[0:3]这样的操作转换成OpSliceMake操作。

字面量

[]int{1, 2, 3}

会在编译期间被转换成这样:

var vstat [3]int
vstat[0] = 1
vstat[1] = 2
vstat[2] = 3
var vauto *[3]int = new([3]int)
*vauto = vstat
slice := vauto[:]
  • 根据切片中的元素数量推断底层数组的大小并创建一个数组
  • 将这些字面量元素存储到初始化的数组中
  • 创建一个[3]int类型的数组指针
  • 将静态存储区的数组vstat赋值给vauto指针所在的地址
  • 通过[:]获取一个切片

关键字

前面的大部分操作的编译期间完成,而关键字大部分在runtime完成。

具体步骤如下:

  • 使用lxy := make([]int, 10)

  • 先判断len是否传入,同时会保证cap一定大于或者等于len

  • 然后判断两点:

    1. 切片大小和容量是否足够小
    2. 切片是否发生了逃逸,最终在堆总初始化

    如果切片发生逃逸或者非常大,会使用runtime.makeslice在堆上初始化。

    如果不发生逃逸或者非常小,会直接转换成这个代码:

    var arr [4]int 
    n := arr[:3]
    

    这个代码会初始化数组并通过下标得到数组对应的切片,在栈或者静态区域创建。然后就转化成了用下标创建。

  • 分支处理完成,进入创建切片的运行时函数runtime.makeslice

    func makeslice(et *_type, len, cap int) unsafe.Pointer {
       mem, overflow := math.MulUintptr(et.size, uintptr(cap))
       if overflow || mem > maxAlloc || len < 0 || len > cap {
          // NOTE: Produce a 'len out of range' error instead of a
          // 'cap out of range' error when someone does make([]T, bignumber).
          // 'cap out of range' is true too, but since the cap is only being
          // supplied implicitly, saying len is clearer.
          // See golang.org/issue/4085.
          mem, overflow := math.MulUintptr(et.size, uintptr(len))
          if overflow || mem > maxAlloc || len < 0 {
             panicmakeslicelen()
          }
          panicmakeslicecap()
       }
        // 如果小于32KB,会在Go语言调度器的P结构体里面初始化,否则会在堆里面初始化
       return mallocgc(mem, et, true)
    }
    

    该函数的主要功能是计算切片占用的内存空间,并在堆中申请一块连续的内存。

    虽然编译期间可以检查出很多错误,但是在创建切片的时候如果发现了这些错误,就出现运行时错误或者崩溃。

    • 内存空间大小发生溢出
    • 申请的内存大于最大可分配内存
    • 传入的长度小于0或者大于容量

访问元素

len(slice)cap(slice)在一些情况下会直接替换成切片的长度或者容量,在编译期完成,不需要在运行时获取。

追加和扩容

  • 如果返回值会覆盖原变量
  • 如果返回值不需要赋值回原变量

如果返回值不需要赋值回原变量

// append(slice, 1, 2, 3)
ptr, len, cap := slice
newlen := len + 3
if newlen > cap {
    ptr, len, cap = growslice(slice, newlen)
    newlen = len + 3
}
*(ptr+len) = 1
*(ptr+len+1) = 2
*(ptr+len+2) = 3
return makeslice(ptr, newlen, cap)

如果返回值会覆盖原变量

// slice = append(slice, 1, 2, 3)
a := &slice
ptr, len, cap := slice
newlen := len + 3
if uint(newlen) > uint(cap) {
   newptr, len, newcap = growslice(slice, newlen)
   vardef(a)
   *a.cap = newcap
   *a.ptr = newptr
}
newlen = len + 3
*a.len = newlen
*(ptr+len) = 1
*(ptr+len+1) = 2
*(ptr+len+2) = 3

可以看到如果返回值回覆盖原变量的话,这里是做了优化的,它是在原切片上动手脚,没有进行复制,提高性能。

当容量不足的时候会调用runtime.growslice函数为切片扩容。

func growslice(et *_type, old slice, cap int) slice {
   if raceenabled {
      callerpc := getcallerpc()
      racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, abi.FuncPCABIInternal(growslice))
   }
   if msanenabled {
      msanread(old.array, uintptr(old.len*int(et.size)))
   }
   if asanenabled {
      asanread(old.array, uintptr(old.len*int(et.size)))
   }

   if cap < old.cap {
      panic(errorString("growslice: cap out of range"))
   }

   if et.size == 0 {
      // append should not create a slice with nil pointer but non-zero len.
      // We assume that append doesn't need to preserve old.array in this case.
      return slice{unsafe.Pointer(&zerobase), old.len, cap}
   }

   newcap := old.cap
   doublecap := newcap + newcap
   if cap > doublecap {
      newcap = cap
   } else {
      const threshold = 256
      if old.cap < threshold {
         newcap = doublecap
      } else {
         // Check 0 < newcap to detect overflow
         // and prevent an infinite loop.
         for 0 < newcap && newcap < cap {
            // Transition from growing 2x for small slices
            // to growing 1.25x for large slices. This formula
            // gives a smooth-ish transition between the two.
            newcap += (newcap + 3*threshold) / 4
         }
         // Set newcap to the requested cap when
         // the newcap calculation overflowed.
         if newcap <= 0 {
            newcap = cap
         }
      }
   }
  • 如果期望容量大于当前容量的2倍,就会使用期望容量
  • 如果当前切片的长度小于1024,就会将容量翻倍
  • 如果大于1024,就会每次增加25%的容量,直到新容量大于期望容量

上面的过程只会大概确定切片的容量,还需要根据切片中的元素大小进行内存对齐

复制切片

要注意大切片复制导致的内存开销问题

总结:不管复制切片的过程是运行时还是非运行时进行的。本质上都是使用runtime.memmove将整块内存复制到目标内存区域中。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/74227.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Security实现前后端分离

Security实现前后端分离 说明 ​ 上一篇和上上一篇我大致介绍了一下security基础使用和oauth2的一些流程&#xff0c;这里在深入了解一些相关的配置项。 ​ 首先我们在梳理一下相关概念&#xff0c;首先基本的security是负责用户认证这这一环节&#xff0c;总而言之就是用户…

PCB入门学习—原理图的绘制1(MCU部分)

目录 2.1 STM32F103VET6 MCU核心电路的绘制 学习目录 2.1 STM32F103VET6 MCU核心电路的绘制 总结&#xff1a;放置元件&#xff0c;连线&#xff0c;放置网络标号&#xff0c;更新序号。 主控放上去之后原理图图纸不太够&#xff1a;双击右边边缘&#xff0c;默认图纸大小是…

【Java小案例】从简到精完美判断年份是闰/平年和该年二月份有几天

目录前言问题描述思路分析解决方案方案一方案二方案三方案四结语前言 1、平年指阳历没有闰日或农历没有闰月的年份&#xff0c;闰年是公历中的名词&#xff0c;是为了弥补因人为历法规定造成的年度天数与地球实际公转周期的时间差而设立的&#xff0c;补上时间差的年份为闰年&a…

QA | SWCF2022 笔记:GNSS模拟赋能汽车HIL测试

2022年度SWCF卫星通信与仿真测试研讨会正在进行中&#xff0c;精彩演讲&#xff1a;GNSS模拟赋能汽车HIL测试&#xff0c;感谢大家的观看与支持&#xff01;收到一些粉丝的技术问题&#xff0c;我们汇总了热点问题并请讲师详细解答&#xff0c;在此整理分享给大家&#xff01; …

高通平台开发系列讲解(UART篇)高速串口代码流程

文章目录 一、初始化1.1、Registration with the SPS driver1.2、UART port registration二、Port open三、Port close沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇章主要介绍高通平台高速串口代码流程。 一、初始化 初始化流程: msm_serial_hs_init() ->

数据传送指令MOV、XCHG

学习过程中要重点掌握对标志寄存器的影响 数据传送类指令&#xff08;不影响标志位&#xff09; 一&#xff1a;MOV指令 先要知道图片中这几个英文表示什么 立即数&#xff08;immediaate operand&#xff09; 寄存器&#xff08;register&#xff09; 内存&#xff08;…

WEB前端大作业HTML静态网页设计旅游景点区主题——三亚旅游网页设计

家乡旅游景点网页作业制作 网页代码运用了DIV盒子的使用方法&#xff0c;如盒子的嵌套、浮动、margin、border、background等属性的使用&#xff0c;外部大盒子设定居中&#xff0c;内部左中右布局&#xff0c;下方横向浮动排列&#xff0c;大学学习的前端知识点和布局方式都有…

基于Java+Swing实现超级玛丽游戏

基于JavaSwing实现超级玛丽游戏一、系统介绍二、功能展示三、其他系统一、系统介绍 超级玛丽小游戏的JAVA程序&#xff0c;进入游戏后首先按空格键开始&#xff0c;利用方向键来控制的马里奥的移动&#xff0c;同时检测马里奥与场景中的障碍物和敌人的碰撞&#xff0c;并判断马…

JVM八股文,面试会被问到什么?都在这里啦 ~

目录 1、JVM内存划分 1.1、程序计数器&#xff08;Program Counter Register&#xff09; 1.2、方法区&#xff08;Method Area&#xff09; 1.3、本地方法栈&#xff08;Native Method Stacks&#xff09; 1.4、虚拟机栈&#xff08;JVM Stacks&#xff09; 1.5、Java堆…

溢出的文字省略号显示

溢出的文字省略号显示 1、单行文本溢出显示省略号 源代码 必须满足三个条件&#xff1a;white-space: nowrap; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; nowrap强制一行内显示文本(默认normal自动换行)&#xff0…

vscode跨语言调试

所谓“工欲善其事&#xff0c;必先利其器”&#xff0c;作为一个程序员&#xff0c;调试在项目开发过程中的重要性自然是不言而喻的。 最近项目中遇到的项目是由python和cpp完成的&#xff0c;python端会调用到cpp的库。由于做二次开发需要进行跨语言联调&#xff0c;所以在这…

QList与QVector遍历方法与性能比较

目录 一、 性能测试 二、 QList与QVector耗时对比分析 三、QList遍历方式对比分析 四、QVector遍历方式对比分析 一、 性能测试 最近使用opengl画点云数据时发现比较卡顿&#xff0c;原因是我使用了QList数据结构&#xff0c;后面改为QVector改善很多&#xff0c;速度提升1倍。…

什么是数学思维

什么是数学 数学 [英语&#xff1a;mathematics&#xff0c;源自古希腊语μάθημα&#xff08;mthēma&#xff09;&#xff1b;经常被缩写为math或maths]&#xff0c;是研究数量、结构、变化、空间以及信息等概念的一门学科。 数学 是人类对事物的抽象结构与模式进行严格…

大学生影视主题网页制作 HTML+CSS+JS仿360影视网站 dreamweaver电影HTML网站制作

HTML实例网页代码, 本实例适合于初学HTML的同学。该实例里面有设置了css的样式设置&#xff0c;有div的样式格局&#xff0c;这个实例比较全面&#xff0c;有助于同学的学习,本文将介绍如何通过从头开始设计个人网站并将其转换为代码的过程来实践设计。 文章目录一、网页介绍一…

数据结构:排序

目录 插入排序 插入排序 希尔排序 选择排序 选择排序 堆排序 交换排序 冒泡排序 快速排序 递归实现&#xff1a; ●hoare版本 ●三数取中小区间法优化 ●挖坑版本 ●双指针版本 非递归 ●用栈实现 ●用队列实现 归并排序 ● 递归 ●非递归 总结 来了朋友&a…

slot的理解

首先&#xff0c;大概了解一下插槽&#xff1a; 插槽是什么 ![在这里插入图片描述](https://img-blog.csdnimg.cn/90b04660769e49c286ee2e1821d2a2bb.png 插槽&#xff1a;在HTML中 slot 元素 &#xff0c;作为 Web Components 技术套件的一部分&#xff0c;是Web组件内的一个占…

HashMap1.8也会发生死循环

在网上搜资料时候然后发现网上都说1.7版本的HashMap会发生死链也就是死循环&#xff0c;但是在HashMap中也会产生死循环&#xff0c;接下来直接看代码吧 代码 类名字我忘记改了这是我以前看park时候弄的但是这不重要 当你运行 public class parkAndUnpark {static Map<…

微服务守护神-Sentinel-降级规则

引言 书接上篇 微服务守护神-Sentinel-流控规则 &#xff0c;上面介绍了Sentinel流控规则&#xff0c;本篇继续来Sentinel的降级规则。 降级规则 那啥为降级呢&#xff1f;降级可以理解为下降等次&#xff0c;比如&#xff1a;你从广州到北京&#xff0c;有钱时&#xff0c;…

Kafka的认证

Kafka支持基于SSL和基于SASL的安全认证机制。 基于SSL的认证主要是指Broker和客户端的双路认证。即客户端认证broker的证书&#xff0c;broker也认证客户端的证书。 Kafka还支持通过SASL做客户端认证。SASL是提供认证和数据安全服务的框架。Kafka支持的SASL机制有5种&#xff…

Docker容器化技术入门(一)Docker简介

Docker容器化技术入门&#xff08;一&#xff09;Docker简介前言&#xff08;一&#xff09;Docker简介1 Docker是什么&#xff1f;1.1 Docker的出现1.2 Docker的理念1.3 一句话2 容器与虚拟机比较2.1 容器发展简史2.2 传统虚拟机技术2.3 容器虚拟化技术2.4 对比3 Docker能干什…