前言:
此刻同学们对“go语言数据分析”都比较注意,各位老铁们都想要知道一些“go语言数据分析”的相关文章。那么小编同时在网络上搜集了一些关于“go语言数据分析””的相关知识,希望朋友们能喜欢,咱们快快来学习一下吧!透过语言看原理,本文会借go语言,来说明下底层是现在基本数据结构的原理
字符串
首先我们需要知道c语言和go中对于字符串结构的不同,
c语言中是一块连续内存,以”\0”结尾,而go中则是两块地址,第一块是16字节,8字节的指针,8字节的长度,可以看下面的代码来验证:
接着我们用gdb调试代码,指令
go build -gcflags "all=-N -l" -o test1 test1.go
gdb test1
下面是具体的一些指令
(gdb) ptype s
type = struct string {
uint8 *str;
int len;
}
(gdb) x/2xg &s
0xc420047f68: 0x000000000106cae9 0x0000000000000007
# 此处 0x000000000106cae9 是字符串地址,0x0000000000000007 是长度
(gdb) x/7cb 0x000000000106cae9
0x106cae9 <go.string.*+601>: 97 'a' 98 'b' 99 'c' 100 'd' 101 'e' 102 'f' 103 'g'
(gdb) info files
0x0000000001001000 - 0x00000000010501f3 is .text
0x0000000001050200 - 0x00000000010740b2 is __TEXT.__rodata
# 可以看到字符串“abcdefg”是保存在__rodata中的
(gdb) p/x $rsp
$4 = 0xc420047f58
(gdb) p/x $rbp
$5 = 0xc420047f78
(gdb) p/x &s
$6 = 0xc420047f68
# 栈基址是0xc420047f78,向下增长到0xc420047f58,其中s是在栈中
(gdb) x/8xw 0xc420047f58
0xc420047f58: 0x2007a000 0x000000c4 0x0102dbc5 0x00000000
0xc420047f68: 0x0106cae9 0x00000000 0x00000007 0x00000000
# 可以看到确实是字符串s的内容
x的用法可以用help x查看
具体的演示视频可以看
[![asciicast]()]()
下面来回答一个问题,为什么所有语言把字符串实现为不可变类型?
提高效率,通过相同字符串都引用同一副本,有效的减少了相同字符串的副本数量提高字符串比较的效率,通过计算hashcode,不同字符串的hashcode肯定不同,相同的hashcode,再比较字符串安全性,不管经过多少次传递,字符串的地址不会改变,防止了恶意的篡改
既然字符串都是不可变的,那对于字符串拼接的操作,我们需要怎么呢?
申请一块内存 buffer,将不可变字符串内容拷贝进来然后修改这块内存 buffer 中的内容再将这块 buffer 中的内容转化为不可变的字符串
可以看到对于一次字符串拼接,需要申请两次内存,那有什么优化方法呢?
下面来看下go中对于字符串拼接的处理。
func main() {
s1 := "aaa"
s2 := "bbb"
s3 := "ccc"
println(s1 + s2 + s3)
}
我们进行汇汇编后看具体的指令。
上面操作就是将字符串在栈上建立起来,然后3个字面量动态分配的地址是:
0x106cbe8,0x106cbeb,0x106cbee。
最后会调用到runtime.concatstring3函数:
func concatstring3(buf *tmpBuf, a [3]string) string {
return concatstrings(buf, a[:])
}
具体看汇编指令,以及相应的栈内存布局:
相应的栈布局:
可以看到栈上需要预留32字节的buf,以及3个字符串,其中返回值concatstrings是concatstrings函数的栈上内容。
通过上面的汇编指令,我们可以看到go中对于字符串拼接,是将其都复制到一块内存中,然后最后再将其转换为字符串返回,其中只有一次内存分配。
现在我们知道了go语言中对于字符串拼接的优化后,我们下面来看字符串与切片之间的转换,先看代码:
func toString(b []byte) string {
return string(b)
}
func unsafeToString(b []byte) string {
return *(*string)(unsafe.Pointer(&b))//b指针的数据结构
}
func main() {
b := []byte("hello, world!")
s1 := toString(b)
s2 := unsafeToString(b)
println(s1 == s2)
println(s1, s2)
}
我们看汇编指令
可以看到是生成了一个string,然后再通过runtime.stringtoslicebyte将其转换为切片,看实现:
func stringtoslicebyte(buf *tmpBuf, s string) []byte {
var b []byte
if buf != nil && len(s) <= len(buf) {
*buf = tmpBuf{} // 将buf内容清零
b = buf[:len(s)] // 此处将会赋值 b 的 cap,len,ptr字段
} else {
b = rawbyteslice(len(s))
}
copy(b, s) # runtime.memmove,直接汇编实现 memmove_amd64.s
# func copy(dst, src []Type) int
return b
}
通过上面我们以看到正常string如何转换为slice的,将string的ptr所指向内容拷贝到slice的ptr所指向的内存。
在内存布局上,slice和string的结构如下:
所以我们可以通过共用底层的byte array,达到高性能的转换。
总结下:上面我们看到了go在实现底层字符串上,通过将字符串不可变,能够极大的提高效率,同时我们如果想修改字符串,则需要通过重新申请内存的拷贝的方式来进行修改,同时我们可以看到,go语言对于32字节的内存,直接是在栈上进行内存分配的,这是对于短字符串的优化。另外,我们看到切片和字符串其结构布局是类似的,只是切片需要多一个cap来管理当前可用的最大内存,通过一些指针转换,我们可以极大的提高字符串和切片之间的转换效率。
数组
看完字符串和切片后,我们来看数组,首先也是c语言和go中数组的不同:
c语言中数组A,A代表了数组首地址,而go中数组A,A代表是整个数组元素,这就带来了在c语言中传递数组,传递的是指针,而go中传递数组,传递的是整个数组。
因此我们来看两种传递数组的不同:
func test1(x [3]int) { // 传递整个数组
println("test:", &x)
x[1] += 100
}
func test2(x *[3]int) { // 传递数组首地址
println("test:", x)
x[1] += 100
}
数组指针和指针数组
什么是指针?指针也是一个变量,只不过这个变量里存的是一个地址,所以数组指针是一个指针,里面存的是
数组地址,而指针数组是一个数组,数组中每个元素都是指针
如何对数组进行动态扩容?
当我们原先的数组容量不够的时候,我们需要重新申请一块内存,然后将数据拷贝过来。而为了使用这块新申请的内存,c语言可以使用指针类型转换,将新申请的内存使用起来,但是go语言默认是不支持指针的,所有需要有另一种结构来管理这块新申请的内存,这就是切片,切片的定义如下:
type slice struct {
array unsafe.Pointer
len int
cap int
}
其中array是指针,指向新申请的内存,len是这块内存的当前使用长度,cap是总的可使用长度。
go中new和make创建引用类型的区别
引用类型就是当前类型内部有一个指针引用另外一个数据结构,在go语言中有两种方式分配内存:new和make。
其中new是返回要分配类型的长度,具体说就是:new([8]byte)返回8byte,new([]byte)计算类型长度 (ptr(8bit)+len(8bit)+cap(8bit)),三个字段组成的24字节内存空间。
make则是一个语法糖,对于引用类型不仅申请类型本身,还会去申请所引用的内存,make([]byte,0,8)时候,首先会创建切片本身头对象 (ptr,len,cap),然后创建底层数组,数组容量是8个,然后把指针指向开始位置,len 设为0,cap 设为8。
上面我们知道了数组的结构,数组的扩容,已经数组的分配,数组在go中只支持静态数组,即数组大小必须是编译期就能够确定的,另外通过切片访问底层数组,会需要额外的一次寻址,这在关键地方会影响性能,因为额外的内存寻址可能意味着不能放入 L1-L3 cache,如果直接访问内存,这个速度就会慢很多。
最后附上一张常用硬件性能参数图。
标签: #go语言数据分析