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 千行代码实现的迷你文本编辑器。该项…

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

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

【嵌入式开发 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锁定代码…

【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],…

web component - 使用HTML Templates和Shadow DOM构建现代UI组件

Web Component是一种用于构建可重用的UI组件的技术。它使用标准化的浏览器API&#xff0c;包括Custom Elements、Shadow DOM和HTML Templates来实现组件化开发方式。这些API都是现代浏览器原生支持的&#xff0c;因此不需要引入第三方库或框架即可使用。 在这篇博客中&#xf…

第6课 用window API捕获麦克风数据并加入队列备用

今天是2024年1月1日&#xff0c;新年的第一缕阳光已经普照大地&#xff0c;祝愿看到这篇文章的所有程序员或程序爱好者都能在新的一年里持之以恒&#xff0c;事业有成。 今天也是我加入CSDN的第4100天&#xff0c;但回过头看一看&#xff0c;这么长的时间也没有在CSDN写下几篇…

SpringCloud-高级篇(九)

&#xff08;1&#xff09;Seata高可用 我们学习了Seata的各种用法了&#xff0c;Seata的服务是单节点部署的&#xff0c;这个服务如果挂了&#xff0c;整个事务都没有办法完了&#xff0c;下面我们学习Seata的高可用的知识。 实现高可用&#xff0c;还是比较简单&#xff0c;…

QT上位机开发(抽奖软件)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 用抽奖软件抽奖&#xff0c;是一种很常见的抽奖方式。特别是写这篇文章的时候&#xff0c;正好处于2023年12月31日&#xff0c;也是一年中最后一天…

【华为机试】2023年真题B卷(python)-喊七的次数重排

一、题目 题目描述&#xff1a; 喊7是一个传统的聚会游戏&#xff0c;N个人围成一圈&#xff0c;按顺时针从1到N编号。 编号为1的人从1开始喊数&#xff0c;下一个人喊的数字为上一个人的数字加1&#xff0c;但是当将要喊出来的数字是7的倍数或者数字本身含有7的话&#xff0c;…

新手快速上手掌握基础排序<二>快速排序快速入门

目录 引言 一&#xff1a;快速排序qsort的简介 1.qsort是一个库函数 2.库函数的查询了解方法 3.qsort的具体使用方法 4.qsort函数使用的一些注意点 5.qsort函数的特点 6.代码实现 (1)整数数组的快速排序 &#xff08;2&#xff09;结构体的快速排序&#xff08;学…