go slice源码探索(切片、copy、扩容)和go编译源码分析

文章目录

    • 概要
    • 一、数据结构
    • 二、初始化
        • 2.1、字面量
        • 2.2、下标截取
            • 2.2.1、截取原理
        • 2.3、make关键字
            • 2.3.1、编译时
    • 三、复制
        • 3.1、copy源码
    • 四、扩容
        • 4.1、append源码
    • 五:切片使用注意事项
    • 六:参考

概要

Go语言的切片(slice)是对数组的扩展,类似C语言常见的简单动态字符串(典型应用如Redis的string类型),动态扩容是其相对数组的最大优势。
本人在工作过程中,对slice的使用与底层原理有了较为全面的理解,特在这里针对其初始化、扩容、复制等机制进行源码分析。

PS: go V1.20.6

一、数据结构

slice的数据结构非常简单,其提供了和数组一样的下标访问任意元素方式。在运行时,其结构由有一个数组字段,一个长度字段,一个容量字段组成。
最初是在runtime/slice.go文件中:

type slice struct {array unsafe.Pointerlen   intcap   int
}

但是2018年10月份的一次优化cmd/compile: move slice construction to callers of makeslice,如下:
 [Go]move slice construction to callers of makeslice

本次优化运行时结构迁移到reflect/value.go文件中:

// SliceHeader is the runtime representation of a slice.
// It cannot be used safely or portably and its representation may
// change in a later release.
// Moreover, the Data field is not sufficient to guarantee the data
// it references will not be garbage collected, so programs must keep
// a separate, correctly typed pointer to the underlying data.
//
// In new code, use unsafe.Slice or unsafe.SliceData instead.
type SliceHeader struct {Data uintptrLen  intCap  int
}

至今未改,其中Data字段是指向底层数组的指针,Len是当前底层数组使用的长度,Cap是当前底层数组的总长度。

二、初始化

切片有三种初始化方式:

  • 使用字面量初始化新的切片;
  • 通过下标的方式获得数组或截取切片的一部分;
  • 使用关键字 make 创建切片。
2.1、字面量

示例如下:

a := []int64{4, 8, 9, 6, 4}
2.2、下标截取

数组转切片:

a := [5]int64{4, 8, 9, 6, 4}
b := a[:]

从切片截取(截取是遵循左闭右开原则):

a := []int64{4, 8, 9, 6, 4}
//删除第一个元素
b1 := a[1:]
//删除最后一个元素
b2 := a[:len(a)-1]
//删除中间一个元素
n := len(a)/2
b3 := append(a[:n],a[n+1:]...)

这种操作非常高效,不会申请内存,相比b1和b2,b3还会涉及到元素的移动,进而改变了a的内容。

2.2.1、截取原理
a := []int64{4, 8, 9, 6, 4}
b1 := a[1:4]//仅指定长度
b2 := a[1:2:3]//指定长度为1(2-1),容量为2(3-1)。【1标识索引下标1、2标识索引下标2,决定长度、3表示索引下标3,决定容量】

长度和容量变化如下:
go slice 截取变化
如图所示,虽说a、b1、b2的值不是同一个,但底层数组还共用同一段连续的内存块,所以在编码中要注意,这一点我们可以从Go SSA过程中一窥究竟:
【在go的源码和汇编码之间,其实编译器做了很多事情,而ssa(是一种中间代码的表示形式)就是查看编译器优化行为的利器】
先设置下环境变量。

# windows
$env:GOSSAFUNC="main"
# linux
export GOSSAFUNC="main"

再运行以下代码:

package mainimport "fmt"func main() {a := []int64{4, 8, 9, 6, 4}b1 := a[2:4]fmt.Println(b1, len(b1), cap(b1))b2 := a[1:2:3]fmt.Println(b2, len(b2), cap(b2))
}

执行go build main.go就可以得到ssa.html文件,读者可自行试验下,内容太多,我们是看start和opt阶段的就可以:
go-ssa我们只探究b1 := a[2:4]即可,关键两处如下:
start阶段:
go-ssa-start
opt阶段:
go-ssa-opt
对比可以看到,从start阶段到opt阶段,中间码已经简化很多。
从中可以看到:

  • 变量v39表示源码中的变量a;
  • 变量v56表示源码中的变量b1;

那么b1 := a[2:4]如何变化的呢?
在opt阶段可以看到v40=v39,

  1. v49表示源码中的变量b1的长度,已经计算出来真实数值 2(在start阶段还不是呢),v50表示源码中的变量b1的容量,已经计算出来真实数值 3。
  2. v55 通过对变量v40进行OffPtr操作得到一个地址,就是一个指针运算,我们知道a,b1元素是int64的,一个元素8字节。b1相对a是右移了两个元素,就是16字节了。即对a的底层数组指针加16字节,就是b1的底层数组的指针了。
  3. v56 就是整合v49,v50,v55这几个变量到一起了。通过SliceMake 操作会接受四个参数创建新的切片,依次元素类型([]int64)、底层数组指针(v55)、长度(v49)和容量(v50),这也是我们在数据结构一节中提到的切片的几个字段 。

可以看到整个过程并没有重新申请新的内存段,是基于a的底层数组,进行指针运算,调整切片长度和容量的值等操作得到b1,
所以需要注意的是修改新切片b1的数据也会改变原切片a的数据。

所以说b2 := a[:2:3]操作只是改小了切片容量,并不会释放a申请的内存段,这种缩容是伪缩容

2.3、make关键字

提到make的源码,我们第一时间想到的就是Go SDK下的src/runtime/slice.go文件中的makeslice函数,但该函数目前只是申请了一块连续内存(见第一章节2018年10月份的一次优化相关),那么什么地方调用了该函数呢?这就要去看一下Go编译器的源码了。

2.3.1、编译时

Go编译器的执行流程有多个阶段:

  1. 经过词法分析和语法分析得到抽象语法树AST;
  2. 类型检查,包含检查常量、类型和函数名等类型,变量捕获与赋值,函数内联、逃逸分析、闭包重写、遍历函数(有些会导入内建的运行时函数,如runtime.makeslice,runtime.makechan等);
  3. SSA生成;
  4. 机器码生成。

分析何处调用runtime.makeslice函数我们只要分析类型检查阶段

编译器入口文件src/cmd/compile/main.go,代码如下:

func main() {// disable timestamps for reproducible outputlog.SetFlags(0)log.SetPrefix("compile: ")buildcfg.Check()archInit, ok := archInits[buildcfg.GOARCH]if !ok {fmt.Fprintf(os.Stderr, "compile: unknown architecture %q\n", buildcfg.GOARCH)os.Exit(2)}gc.Main(archInit)//注意此处gc是go compiler的缩写,与垃圾回收的GC(garbage collection)区分开base.Exit(0)
}

进入gc.Main函数:

func Main(archInit func(*ssagen.ArchInfo)) {//此处省略若干代码...// Prepare for backend processing. This must happen before pkginit,// because it generates itabs for initializing global variables.ssagen.InitConfig()//ssa初始化// 词法解析、语法解析、类型检查工作noder.LoadPackage(flag.Args())//此处省略若干代码...// 逃逸分析escape.Funcs(typecheck.Target.Decls)//遍历函数工作base.Timer.Start("be", "compilefuncs")fcount := int64(0)for i := 0; i < len(typecheck.Target.Decls); i++ {if fn, ok := typecheck.Target.Decls[i].(*ir.Func); ok {// Don't try compiling dead hidden closure.if fn.IsDeadcodeClosure() {continue}enqueueFunc(fn)fcount++}}base.Timer.AddEvent(fcount, "funcs")//ssa生成、机器码生成工作compileFunctions()// Write object data to disk.base.Timer.Start("be", "dumpobj")dumpdata()base.Ctxt.NumberSyms()dumpobj()if base.Flag.AsmHdr != "" {dumpasmhdr()}
}

进入noder.LoadPackage函数:
该函数位于src/cmd/compile/internal/noder/目录下,

func LoadPackage(filenames []string) {//只摘抄了部分关键代码// Limit the number of simultaneously open files.sem := make(chan struct{}, runtime.GOMAXPROCS(0)+10)noders := make([]*noder, len(filenames))//...// 词法解析、语法解析工作p.file, _ = syntax.Parse(fbase, f, p.error, p.pragma, syntax.CheckBranches) // 类型检查相关check2(noders)
}

check2函数会在某个节点调用typecheck.Expr,typecheck.Stmt,typecheck.Call等函数进行类型检查,即转入typecheck.typecheck函数。

func typecheck(n ir.Node, top int) (res ir.Node) {//省略...n.SetTypecheck(2)n = typecheck1(n, top)n.SetTypecheck(1)//省略...
}
// typecheck1 should ONLY be called from typecheck.
func typecheck1(n ir.Node, top int) ir.Node {switch n.Op() {case ir.OMAKE://make操作n := n.(*ir.CallExpr)return tcMake(n)}
}
// tcMake typechecks an OMAKE node.
func tcMake(n *ir.CallExpr) ir.Node {args := n.Argsl := args[0]l = typecheck(l, ctxType)t := l.Type()var nn ir.Nodeswitch t.Kind() {case types.TSLICE://...,设置为ir.OMAKESLICE操作nn = ir.NewMakeExpr(n.Pos(), ir.OMAKESLICE, l, r)}//省略...return nn
}
func NewMakeExpr(pos src.XPos, op Op, len, cap Node) *MakeExpr {n := &MakeExpr{Len: len, Cap: cap}n.pos = posn.SetOp(op)return n
}

至此获取了make([]int,0,10)之类操作的类型,稍后进入遍历函数操作,即gc.Main函数中的enqueueFunc

func enqueueFunc(fn *ir.Func) {//...todo := []*ir.Func{fn}for len(todo) > 0 {next := todo[len(todo)-1]todo = todo[:len(todo)-1]prepareFunc(next)todo = append(todo, next.Closures...)}//...
}
// prepareFunc handles any remaining frontend compilation tasks that
// aren't yet safe to perform concurrently.
func prepareFunc(fn *ir.Func) {walk.Walk(fn)//进入遍历函数核心逻辑
}

调用链:Walk->walkStmtList->walkStmt->walkExpr->walkExpr1

func walkExpr1(n ir.Node, init *ir.Nodes) ir.Node {switch n.Op() {case ir.OMAKESLICE:n := n.(*ir.MakeExpr)return walkMakeSlice(n, init)case ir.OSLICEHEADER:n := n.(*ir.SliceHeaderExpr)return walkSliceHeader(n, init)}
}
// walkMakeSlice walks an OMAKESLICE node.
func walkMakeSlice(n *ir.MakeExpr, init *ir.Nodes) ir.Node {l := n.Lenr := n.Capif n.Esc() == ir.EscNone {//不发生逃逸,分配栈内内存,注意这里由gc.Main函数中的escape.Funcs函数分析得到t = types.NewArray(t.Elem(), i) // [r]Tvar_ := typecheck.Temp(t)appendWalkStmt(init, ir.NewAssignStmt(base.Pos, var_, nil))  // zero tempr := ir.NewSliceExpr(base.Pos, ir.OSLICE, var_, nil, l, nil) // arr[:l]// The conv is necessary in case n.Type is named.return walkExpr(typecheck.Expr(typecheck.Conv(r, n.Type())), init)}len, cap := l, rfnname := "makeslice64"//声明要调用runtime.makeslice64函数argtype := types.Types[types.TINT64]if (len.Type().IsKind(types.TIDEAL) || len.Type().Size() <= types.Types[types.TUINT].Size()) &&(cap.Type().IsKind(types.TIDEAL) || cap.Type().Size() <= types.Types[types.TUINT].Size()) {fnname = "makeslice"//声明要调用runtime.makeslice函数argtype = types.Types[types.TINT]}fn := typecheck.LookupRuntime(fnname)//调用得到一块连续内存的头指针ptr := mkcall1(fn, types.Types[types.TUNSAFEPTR], init, reflectdata.MakeSliceElemRType(base.Pos, n),   typecheck.Conv(len, argtype), typecheck.Conv(cap, argtype))ptr.MarkNonNil()//修正slice长度和容量len = typecheck.Conv(len, types.Types[types.TINT])cap = typecheck.Conv(cap, types.Types[types.TINT])//这里转化为ir.OSLICEHEADER操作sh := ir.NewSliceHeaderExpr(base.Pos, t, ptr, len, cap)//执行ir.OSLICEHEADER操作return walkExpr(typecheck.Expr(sh), init)
}
// 转化为ir.SliceHeaderExpr,在程序启动后,就会变成反射库中的SliceHeader 结构体
func walkSliceHeader(n *ir.SliceHeaderExpr, init *ir.Nodes) ir.Node {n.Ptr = walkExpr(n.Ptr, init)n.Len = walkExpr(n.Len, init)n.Cap = walkExpr(n.Cap, init)return n
}

至此把编译阶段如何调用makeslice基本解释清楚了,也顺便了解了Go编译相关的知识

至于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 {//参数自动修正mem, overflow := math.MulUintptr(et.size, uintptr(len))if overflow || mem > maxAlloc || len < 0 {panicmakeslicelen()}panicmakeslicecap()}return mallocgc(mem, et, true)//申请一块连续的内存
}
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {//...if size == 0 {//这时mallocgc函数有意思的地方,此时会返回一个固定指针,我们常用的struct{}{}就是因此而来return unsafe.Pointer(&zerobase)}//...
}

PS:ir.SliceHeaderExpr是如何在程序启动后转化为reflect.SliceHeader 的呢?有兴趣的大佬可在评论区解释下

三、复制

func main() {s1 := []string{"aaa", "sss", "ddd"}s2 := make([]string, 2, 6)copy(s2, s1)s2 = append(s2, "yyy")printSlice(s2)//output: len=3 cap=6 slice=[aaa sss yyy]s3 := make([]string, 4, 6)copy(s3, s1)s3 = append(s3, "xxx")printSlice(s3)//output: len=5 cap=6 slice=[aaa sss ddd  xxx]
}
func printSlice(x []string) {fmt.Printf("len=%d cap=%d slice=%v\n", len(x), cap(x), x)
}

根据s2,s3的打印结果可知,若想将源slice的内容全部复制到目的slice,那么目的slice的长度必须大于等于源slice的长度。

3.1、copy源码

编译时源码可见2.3.1小节,关键词是src/cmd/compile/internal/ir/node.go中的OCOPY ,搜索可知其遍历函数是walkCopy

// Lower copy(a, b) to a memmove call or a runtime call.
// Also works if b is a string.
func walkCopy(n *ir.BinaryExpr, init *ir.Nodes, runtimecall bool) ir.Node {if n.X.Type().Elem().HasPointers() {//slice在堆上的话调用runtime.typedslicecopyfn := writebarrierfn("typedslicecopy", n.X.Type().Elem(), n.Y.Type().Elem())return mkcall1(fn, n.Type(), init, reflectdata.CopyElemRType(base.Pos, n), ptrL, lenL, ptrR, lenR)}if runtimecall {//某些特殊情况,比如编译时开启竞态检查(-race),调用runtime.slicecopyfn := typecheck.LookupRuntime("slicecopy")fn = typecheck.SubstArgTypes(fn, ptrL.Type().Elem(), ptrR.Type().Elem())return mkcall1(fn, n.Type(), init, ptrL, lenL, ptrR, lenR, ir.NewInt(n.X.Type().Elem().Size()))}//排除以上两种情况,都走runtime.memmovenlen := typecheck.Temp(types.Types[types.TINT])// n = len(to)l = append(l, ir.NewAssignStmt(base.Pos, nlen, ir.NewUnaryExpr(base.Pos, ir.OLEN, nl)))fn := typecheck.LookupRuntime("memmove")fn = typecheck.SubstArgTypes(fn, nl.Type().Elem(), nl.Type().Elem())call := mkcall1(fn, nil, init, nto, nfrm, nwid)ne.Body.Append(call)return nlen
}

进入runtime.typedslicecopy和runtime.slicecopy函数,其最后也是调用的runtime.memmove函数。
该函数与C语言的memmove作用是一样的,时间复杂度是O(N),所以面对较多元素的切片时,使用copy操作应当慎重。

四、扩容

func main() {r := make([]int, 0, 3)fmt.Printf("len=%d cap=%d slice=%v,r addr:%p,addr:%p\n", len(r), cap(r), r, &r, r) //初始化,但可以看出r本质为*SliceHeader的指针类型,所以在传参时就是指针传递r = append(r, 5, 6)fmt.Printf("len=%d cap=%d slice=%v,r addr:%p,addr:%p,r[0] addr:%p\n", len(r), cap(r), r, &r, r, &r[0]) //第一个元素地址没变r = append(r, 11)fmt.Printf("len=%d cap=%d slice=%v,r addr:%p,addr:%p,r[0] addr:%p\n", len(r), cap(r), r, &r, r, &r[0]) //第一个元素地址没变r = append(r, 22)fmt.Printf("扩容:len=%d cap=%d slice=%v,r addr:%p,addr:%p,r[0] addr:%p\n", len(r), cap(r), r, &r, r, &r[0]) //扩容后地址发生变化,即底层数组发生变化,但变量的地址不变fmt.Printf("r addr:%p,addr:%p,r[0] addr:%p,r[1] addr:%p,\n", &r, r, &r[0], &r[1])                         //r值的地址也变为扩容后第一个元素的地址r = append(r, []int{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110}...)fmt.Printf("扩容 len=%d cap=%d slice=%v,r addr:%p,r[0] addr:%p\n", len(r), cap(r), r, &r, &r[0]) //扩容后地址发生变化,即底层数组发生变化,但变量的地址不变。扩容后newCap本应该是15,但实际是16,因为做了内存对齐
}

运行代码输出如下:

len=0 cap=3 slice=[],r addr:0xc000008570,addr:0xc000017698
len=2 cap=3 slice=[5 6],r addr:0xc000008570,addr:0xc000017698,r[0] addr:0xc000017698
len=3 cap=3 slice=[5 6 11],r addr:0xc000008570,addr:0xc000017698,r[0] addr:0xc000017698
扩容:len=4 cap=6 slice=[5 6 11 22],r addr:0xc000008570,addr:0xc00000eba0,r[0] addr:0xc00000eba0
r addr:0xc000008570,addr:0xc00000eba0,r[0] addr:0xc00000eba0,r[1] addr:0xc00000eba8,
扩容 len=15 cap=16 slice=[5 6 11 22 10 20 30 40 50 60 70 80 90 100 110],r addr:0xc000008570,r[0] addr:0xc000078f00

其扩容流程图如下:
假设有一切片,其长度为oldLen,容量为oldCap,现要增加num个元素。则有newLen=oldLen+num,doublecap=oldCap+oldCap。
go slice扩容

4.1、append源码

编译时源码可见2.3.1小节,关键词是src/cmd/compile/internal/ir/node.go中的OAPPEND ,注意walkExpr1函数源码中OAPPEND已废弃,而是走OAS搜索可知其遍历函数是walkAssign

func walkAssign(init *ir.Nodes, n ir.Node) ir.Node {//...as := n.(*ir.AssignStmt)switch as.Y.Op() {case ir.OAPPEND:var r ir.Nodeswitch {case isAppendOfMake(call):// x = append(y, make([]T, y)...)r = extendSlice(call, init)case call.IsDDD:r = appendSlice(call, init) // also works for append(slice, string).default:r = walkAppend(call, init, as)}}
}
func walkAppend(n *ir.CallExpr, init *ir.Nodes, dst ir.Node) ir.Node {var l []ir.Node// s = slice to append tos := typecheck.Temp(nsrc.Type())l = append(l, ir.NewAssignStmt(base.Pos, s, nsrc))// num = number of things to appendnum := ir.NewInt(int64(argc))// newLen := s.len + numnewLen := typecheck.Temp(types.Types[types.TINT])l = append(l, ir.NewAssignStmt(base.Pos, newLen, ir.NewBinaryExpr(base.Pos, ir.OADD, ir.NewUnaryExpr(base.Pos, ir.OLEN, s), num)))//调用runtime.growslice函数fn := typecheck.LookupRuntime("growslice") //   growslice(ptr *T, newLen, oldCap, num int, <type>) (ret []T)fn = typecheck.SubstArgTypes(fn, s.Type().Elem(), s.Type().Elem())nif.Else = []ir.Node{ir.NewAssignStmt(base.Pos, s, mkcall1(fn, s.Type(), nif.PtrInit(),ir.NewUnaryExpr(base.Pos, ir.OSPTR, s),//要扩容切片的地址newLen,//新切片元素个数ir.NewUnaryExpr(base.Pos, ir.OCAP, s),//要扩容切片的容量num,//要追加的元素个数reflectdata.TypePtr(s.Type().Elem()))),//要扩容切片的类型}
}

再看看runtime.growslice函数

func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {oldLen := newLen - numif et.size == 0 {return slice{unsafe.Pointer(&zerobase), newLen, newLen}//扩容的运行时竟然用的是runtime.slice结构体}//扩容逻辑newcap := oldCapdoublecap := newcap + newcapif newLen > doublecap {newcap = newLen} else {const threshold = 256if oldCap < threshold {newcap = doublecap} else {// Check 0 < newcap to detect overflow// and prevent an infinite loop.for 0 < newcap && newcap < newLen {// 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 = newLen}}}var overflow boolvar lenmem, newlenmem, capmem uintptr//进行内存对齐switch {case et.size == 1:lenmem = uintptr(oldLen)newlenmem = uintptr(newLen)capmem = roundupsize(uintptr(newcap))overflow = uintptr(newcap) > maxAllocnewcap = int(capmem)case et.size == goarch.PtrSize: //goarch.PtrSize is 4 on 32-bit systems, 8 on 64-bit systems。lenmem = uintptr(oldLen) * goarch.PtrSizenewlenmem = uintptr(newLen) * goarch.PtrSizecapmem = roundupsize(uintptr(newcap) * goarch.PtrSize)//内存对齐,overflow = uintptr(newcap) > maxAlloc/goarch.PtrSizenewcap = int(capmem / goarch.PtrSize)case isPowerOfTwo(et.size):var shift uintptrif goarch.PtrSize == 8 {// Mask shift for better code generation.shift = uintptr(sys.TrailingZeros64(uint64(et.size))) & 63} else {shift = uintptr(sys.TrailingZeros32(uint32(et.size))) & 31}lenmem = uintptr(oldLen) << shiftnewlenmem = uintptr(newLen) << shiftcapmem = roundupsize(uintptr(newcap) << shift)overflow = uintptr(newcap) > (maxAlloc >> shift)newcap = int(capmem >> shift)capmem = uintptr(newcap) << shiftdefault:lenmem = uintptr(oldLen) * et.sizenewlenmem = uintptr(newLen) * et.sizecapmem, overflow = math.MulUintptr(et.size, uintptr(newcap))capmem = roundupsize(capmem)newcap = int(capmem / et.size)capmem = uintptr(newcap) * et.size}if overflow || capmem > maxAlloc {panic(errorString("growslice: len out of range"))}//申请新切片所需的内存var p unsafe.Pointerif et.ptrdata == 0 {p = mallocgc(capmem, nil, false)memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)} else {p = mallocgc(capmem, et, true)if lenmem > 0 && writeBarrier.enabled {bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(oldPtr), lenmem-et.size+et.ptrdata)}}//旧切片中的内容复制到新切片中memmove(p, oldPtr, lenmem)return slice{p, newLen, newcap}
}

五:切片使用注意事项

  1. 切片初始化时尽量确定容量;

六:参考

1]:深入学习go语言-前置知识-编译过程

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

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

相关文章

axios的使用及说明

目录 1.说明 2.直接使用 3.封装使用 4.注意 1.说明 官网&#xff1a;Axios 实例 | Axios中文文档 | Axios中文网 Axios 是一个基于 promise 网络请求库&#xff0c;作用于node.js 和浏览器中。 它是 isomorphic 的(即同一套代码可以运行在浏览器和node.js中)。在服务端它使…

Java超高精度无线定位技术--UWB (超宽带)人员定位系统源码

UWB室内定位技术是一种全新的、与传统通信技术有极大差异的通信新技术。它不需要使用传统通信体制中的载波&#xff0c;而是通过发送和接收具有纳秒或纳秒级以下的极窄脉冲来传输数据&#xff0c;从而具有GHz量级的带宽。 UWB&#xff08;超宽带&#xff09;高精度定位系统是一…

java零拷贝zero copy MappedByteBuffer

目录 调用操作系统的 mmap 未使用 mmap 的文件通过网络传输的过程 使用 mmap 的文件通过网络传输的过程 使用例子 调用操作系统的 sendfile() 在 java 中的具体实现 mmap的优劣 mmap 的不足 mmap 的优点 mmap 的使用场景 对于零拷贝&#xff08;zero copy&#xff09…

C语言实验4:指针

目录 一、实验要求 二、实验原理 1. 指针的基本概念 1.1 指针的定义 1.2 取地址运算符&#xff08;&&#xff09; 1.3 间接引用运算符&#xff08;*&#xff09; 2. 指针的基本操作 2.1 指针的赋值 2.2 空指针 3. 指针和数组 3.1 数组和指针的关系 3.2 指针和数…

【Linux】内核编译 镜像制作

文章目录 一、Ubuntu内核编译1.1 为什么自己编译内核1.2 Ubuntu 内核源码下载1.21 内核的作用1.22 Linux内核与ubuntu内核1.23 Ubuntu内核源码获取 1.3 在Windows系统下编译ubuntu内核1.4 在Linux系统下编译ubuntu内核 二、镜像制作 一、Ubuntu内核编译 1.1 为什么自己编译内核…

用LCD循环右移显示“Welcome to China“

#include<reg51.h> //包含单片机寄存器的头文件 #include<intrins.h> //包含_nop_()函数定义的头文件 sbit RSP2^0; //寄存器选择位&#xff0c;将RS位定义为P2.0引脚 sbit RWP2^1; //读写选择位&#xff0c;将RW位定义为P2.1引脚 sbit EP2^2; //使能…

Debezium日常分享系列之:向 Debezium 连接器发送信号

Debezium日常分享系列之&#xff1a;向 Debezium 连接器发送信号 一、概述二、激活源信号通道三、信令数据集合的结构四、创建信令数据集合五、激活kafka信号通道六、数据格式七、激活JMX信号通道八、自定义信令通道九、Debezium 核心模块依赖项十、部署自定义信令通道十一、信…

【C# 技术】 C# 常用排序方式——自定义数据排序

C# 常用排序方式——自定义数据排序 前言 在最近的项目中经常会对C#中的数据进行排序&#xff0c;对于基本数据类型&#xff0c;其排序方式比较简单&#xff0c;只需要调用内置算法即可实现&#xff0c;但对于自定义数据类型以及自定义排序规则的情况实现起来就比较麻烦&…

区分LR(0),SLR(1),LR(1)和LALR(1)

目录 对于LR(0)文法&#xff1a; 对于SLR(1)文法&#xff1a; 对于LR(0)和SLR(1)文法&#xff1a; 对于LR(1)和SLR(1)文法&#xff1a; 对于LALR(1)文法&#xff1a; 例题1&#xff1a; 例题2&#xff1a; 例题3&#xff1a; 例题4&#xff1a; 这几个文法大致的步骤都…

推荐几个贼有意思的开源项目!

这里有实战项目、入门教程、黑科技、开源书籍、大厂开源项目等&#xff0c;涵盖多种编程语言 Python、Java、Go、C/C、Swift...让你在短时间内感受到开源的魅力&#xff0c;对编程产生兴趣&#xff01; C 项目 1、kilo&#xff1a;不到 1 千行代码实现的迷你文本编辑器。该项…

STL容器——map

由于某些原因其实就是作者咕咕咕了 &#xff0c;这篇文章到2023年的最后一天才更出来 map的认识 关于map&#xff0c;这是一种常用的工具 基本上可以看成一个下标可以为任意的数组 注意&#xff0c;这里的任意&#xff0c;包括C中所有类型的所有取值可能 也就是说下标是什…

Portraiture4.1汉化版PS磨皮插件(支持原生m1芯片m2)

Portraiture汉化版PS磨皮插件。本期推荐一款全新ai算法ps2024中文汉化版ps磨皮插件Portraiture 4.1.2美颜滤镜安装包最新版ps调整肤色插件! 全新Portraiture 4.1.2版本PS人像修图美颜磨皮插件&#xff0c;升级AI算法&#xff0c;并支持多人及全身磨皮美化模式&#xff0c;推荐…

c++对c的加强

目录 提出了命名空间的概念 实用性增强 register关键字增强 变量检测增强 struct类型加强 C中所有的变量和函数都必须有类型 新增bool数据类型 提出了命名空间的概念 命名空间将全局作用域分成不同的部分 不同命名空间中的标识符可以同名而不会发生冲突 命名空间可以相互…

TypeScript 类方法装饰器

type ClassMethodDecorator (value: Function,context: {kind: method;name: string | symbol;static: boolean;private: boolean;access: { get: () > unknown };addInitializer(initializer: () > void): void;} ) > Function | void; 1、如果装饰器返回一个函数就…

C# 中 async/await 遇上 forEach 两种写法,是否按照遍历?

在 C# 中&#xff0c;async/await 与 forEach 可以搭配使用&#xff0c;但需要注意的是&#xff0c;forEach 本身不是一个异步操作&#xff0c;它会按顺序同步地遍历集合中的元素&#xff0c;并将每个元素作为参数传递给回调函数。因此&#xff0c;如果在 forEach 循环中使用 a…

【嵌入式开发 Linux 常用命令系列 7.3 -- linux 命令行数值计算】

文章目录 linux 命令行数值计算使用 awk使用 bc 命令使用 Bash 的内置算术扩展使用 expr脚本命令实现 linux 命令行数值计算 在 Linux 命令行中&#xff0c;您可以使用多种方法来执行基本的数学运算。以下是一些示例&#xff1a; 使用 awk awk 是一个强大的文本处理工具&…

【C#】知识点实践序列之Lock的输出多线程信息

大家好&#xff0c;我是全栈小5&#xff0c;欢迎来到《小5讲堂之知识点实践序列》文章。 2023年第2篇文章&#xff0c;此篇文章是C#知识点实践序列之Lock知识点&#xff0c;博主能力有限&#xff0c;理解水平有限&#xff0c;若有不对之处望指正&#xff01; 本篇在Lock锁定代码…

CAN,SPI,IIC,USART每帧的组成

字节是计算机中用于存储数据的基本单位&#xff0c;一个字节由8个二进制位组成。在计算机科学中&#xff0c;字节的大小是固定的&#xff0c;即1字节8位。1比特1位 在不同的数据类型中&#xff0c;字节的大小也不同。例如&#xff0c;在ASCII码中&#xff0c;一个英文字母或数…

【python_数据分组】

对excel按照标签进行分组&#xff0c;例如按照“开票主体和对方公司”进行分组&#xff0c;并获取对应的明细。 表格如下&#xff1a; def main(excel_data):result {}for d in excel_data:if str(d[0])str(d[1]) in result:result[str(d[0])str(d[1])].append([d[0],d[1],…

Java 语法糖的介绍

在Java编程中&#xff0c;语法糖是一种简化代码的技巧&#xff0c;它可以使代码更易读、易写&#xff0c;同时提高开发效率。尽管从语法上看&#xff0c;它更像是一种装饰&#xff0c;但它能给我们的代码带来革命性的改变。 一、什么是Java语法糖&#xff1f; "语法糖&q…