golang中的内存对齐(进阶必看)

先看一个结构体

// 写法一
type T1 struct {
	a int8
	b int64
	c int16
}

// 写法二
type T2 struct {
	a int8
	c int16
	b int64
}

对于这两个结构体,都有a、b、c三个定义完全一样的字段,只是在定义结构体的时候字段顺序不一样而已,那么两种写法有什么影响吗?

对于新手来说,感觉着没有什么区别的,只是一个书写顺序不同而已,但对于go编译器来说,则有着很大的区别,特别是在不同架构上(32位/64位)的编译器,在一定程序上对内存的使用大小和执行效率有着一定的不同。这里的主要知识点就是golang语言中的内存对齐概念(alignment guarantee),https://gfw.go101.org/article/memory-layout.html

类型的尺寸和结构体字节填充(structure padding)

Go白皮书只对以下种类的类型的尺寸进行了明确规定

类型种类                  尺寸(字节数)
------                   ------
byte, uint8, int8        1
uint16, int16            2
uint32, int32, float32   4
uint64, int64            8
float64, complex64       8
complex128               16
uint, int                取决于编译器实现。通常在
                         32位架构上为4,在64位
                         架构上为8。
uintptr                  取决于编译器实现。但必须
                         能够存下任一个内存地址。

Go白皮书没有对其它种类的类型的尺寸最初明确规定。 请阅读值复制成本一文来获取标准编译器使用的各种其它类型的尺寸。

标准编译器(和gccgo编译器)将确保一个类型的尺寸为此类型的对齐保证的倍数。

为了满足上一节中规定的地址对齐保证要求,Go编译器可能会在结构体的相邻字段之间填充一些字节。 这使得一个结构体类型的尺寸并非等于它的各个字段类型尺寸的简单相加之和。

下面是一个展示了一些字节是如何填充到一个结构体中的例子。 首先,从上面的描述中,我们已得知(对于标准编译器来说):

  • 内置类型int8的对齐保证和尺寸均为1个字节; 内置类型int16的对齐保证和尺寸均为2个字节; 内置类型int64的尺寸为8个字节,但它的对齐保证在32位架构上为4个字节,在64位架构上为8个字节。
  • 下例中的类型T1T2的对齐保证均为它们的各个字段的最大对齐保证。 所以它们的对齐保证和内置类型int64相同,即在32位架构上为4个字节,在64位架构上为8个字节。
  • 类型T1T2尺寸需为它们的对齐保证的倍数,即在32位架构上为4n个字节,在64位架构上为8n个字节。
type T1 struct {
	a int8

	// 在64位架构上,为了让下一个字段b的地址为8字节对齐,
	// 需在在字段a这里填充7个字节。在32位架构上,为了让
	// 字段b的地址为4字节对齐,需在这里填充3个字节。

	b int64
	c int16

	// 为了让类型T1的尺寸为T1的对齐保证的倍数,
	// 在64位架构上需在这里填充6个字节,在32架构
	// 上需在这里填充2个字节。
}
// 类型T1的尺寸在64位架构上位24个字节(1+7+8+2+6),
// 在32位架构上为16个字节(1+3+8+2+2)。
// 以保存每个字段都是8(64位架构)或者4(32位架构)的的整数倍

type T2 struct {
	a int8

	// 为了让下一个字段c的地址为2字节对齐,
	// 需在字段a这里填充1个字节。

	c int16

	// 在64位架构上,为了让下一个字段b的地址为8字节对齐,
	// 需在字段c这里填充4个字节。在32位架构上,不需填充
	// 字节即可保证字段b的地址为4字节对齐的。

	b int64
}
// 类型T2的尺寸在64位架构上位16个字节(1+1+2+4+8),
// 在32位架构上为12个字节(1+1+2+8)。

从这个例子可以看出,尽管类型T1T2拥有相同的字段集,但是它们的尺寸并不相等。每个字段的大小都要受下一个字段大小的影响,以方便下个字段对齐。所以建议在开发中,字段占用空间小的放在前面。

一个有趣的事实是有时候一个结构体类型中零尺寸类型的字段可能会影响到此结构体类型的尺寸。 请阅读此问答获取详情。

如果还有些模糊的话可以看一下这篇文章:https://blog.csdn.net/Lazyboy_/article/details/88579966

goroutine和线程区别

从调度上看,goroutine的调度开销远远小于线程调度开销。

OS的线程由OS内核调度,每隔几毫秒,一个硬件时钟中断发到CPU,CPU调用一个调度器内核函数。这个函数暂停当前正在运行的线程,把他的寄存器信息保存到内存中(暂时保存线程状态),查看线程列表并决定接下来运行哪一个线程,再从内存中恢复线程的注册表信息,最后继续执行选中的线程。这种线程切换需要一个完整的上下文切换:即保存一个线程的状态到内存,再恢复另外一个线程的状态,最后更新调度器的数据结构。某种意义上,这种操作还是很慢的。

OS 线程调度器

Go运行的时候包涵一个自己的调度器,这个调度器使用一个称为一个M:N调度技术,m个goroutine到n个os线程(可以用GOMAXPROCS来控制n的数量),Go的调度器不是由硬件时钟来定期触发的,而是由特定的go语言结构来触发的,他不需要切换到内核语境,所以调度一个goroutine比调度一个线程的成本低很多。

从栈空间上,goroutine的栈空间更加动态灵活。

每个OS的线程都有一个固定大小的栈内存,通常是2MB,栈内存用于保存在其他函数调用期间哪些正在执行或者临时暂停的函数的局部变量。这个固定的栈大小,如果对于goroutine来说,可能是一种巨大的浪费。作为对比,goroutine在生命周期开始只有一个很小的栈,典型情况是2KB, 在go程序中,一次创建十万左右的goroutine也不罕见(2KB*100,000=200MB)。而且goroutine的栈不是固定大小,它可以按需增大和缩小,最大限制可以到1GB。

参考:https://time.geekbang.org/course/detail/160-86799

goroutine没有一个特定的标识。

在大部分支持多线程的操作系统和编程语言中,线程有一个独特的标识,通常是一个整数或者指针,这个特性可以让我们构建一个线程的局部存储,本质是一个全局的map,以线程的标识作为键,这样每个线程可以独立使用这个map存储和获取值,不受其他线程干扰。

goroutine中没有可供程序员访问的标识,原因是一种纯函数的理念,不希望滥用线程局部存储导致一个不健康的超距作用,即函数的行为不仅取决于它的参数,还取决于运行它的线程标识。

reference: 《Go程序设计语言》E-mail: huahuiyang@gmail.com https://www.linkedin.com/in/huahuiyang/

转自:https://www.cnblogs.com/yanghuahui/p/9043631.html

Golang中strcut结构体的的值方法和指针方法

平时我们在写struct的时候,经常会用到一些方法,有些方法是我们熟悉的普通方法,在golang中我们称之为值方法,而另一种则是指针方法。

type Person struct {
    Firstname string
    Lastname string
    Age uint8
}
// 值方法
func (p Person) show() {
    fmt.Println(p.Firstname)
}

// 指针方法
func (p *Person) show2() {
    fmt.Println(p.Firstname)
}

可以看到所谓的值方法与指针方法在编写的时候,只是有无*号的区别,这个*就是指针的意思。

那么用法又有何不同呢?

// 值方法
func (p Person) setFirstName(name string) {
	p.Firstname = name
}
// 指针方法
func (p *Person) setFirstName2(name string) {
	p.Firstname = name
}

func main() {
	p := Person{"sun", "xingfang", 30}

	//不一致的情况
	p.show() // sun 修改前
	p.setFirstName("tom")	// 值方法
	p.show() // sun, 未变化

	p.show() // sun 修改前
	p.setFirstName2("tom")	// 指针方法
	p.show() // tom 修改后的tom
}

通过上面的输出我们可以看到,当调用值方法setFirstName后,输出的还是原来的值sun,而调用指针方法 setFirstNam2后,则输出的是新值。主要原因就是值方法在传递总结体的时候,用的只是原来结构体的一个副本,做的任何修改也只是对副本的修改,而打印的还是原来的结构体,两者互不影响。而指针方法,传递的则是指向结构体指针的值副本,指针值一样(X012242R424),指定的都是底层的数据结构,所以才会出现这种情况。

总结:

  1. 值方法的接收者是该方法所属的那个类型值的一个副本。我们在该方法内对该副本的修1改一般都不会体现在原值上,除非这个类型本身是某个引用类型(比如切片或字典)的别名类型。 而指针方法的接收者,是该方法所属的那个基本类型值的指针值的一个副本。我们在这样的方法内对该副本指向的值进行修改,却一定会体现在原值上。
  2. 一个自定义数据类型的方法集合中仅会包含它的所有值方法,而该类型的指针类型的方法集合却囊括了前者的所有方法,包括所有值方法所有指针方法
    严格来讲,我们在这样的基本类型的值上只能调用到它的值方法
    但是,Go 语言会适时地为我们进行自动地转译,使得我们在这样的值上也能调用到它的指针方法。本示例中的 p.show() 在调用的时候会自动转换成 show2() 这种指针方式,可以试着将例子中的 show() 改成 show2() 输出结果是一样的)比如,在Cat类型的变量cat之上,之所以我们可以通过cat.SetName(“monster”)修改猫的名字,是因为 Go 语言把它自动转译为了(&cat).SetName(“monster”),即:先取cat的指针值,然后在该指针值上调用SetName方法。以上是由“郝林”老师在“Go语言核心36讲”专栏中总结。
  3. 两种写法在使用接口的时候也会有所不同。

Golang中的unsafe.Sizeof()简述

测试环境:
系统 win7 64位
go version: go1.10 windows/amd64

我们先看一下代码的输出

package main

import "unsafe"

func main() {
	// string
	str1 := "abc"
	println("string1:", unsafe.Sizeof(str1)) // 16
	str2 := "abcdef"
	println("string2:", unsafe.Sizeof(str2)) // 16
	
	// 数组
	arr1 := [...]int{1, 2, 3, 4}
	println("array1:", unsafe.Sizeof(arr1)) // 32 = 8 * 4

	arr2 := [...]int{1, 2, 3, 4, 5}
	println("array2:", unsafe.Sizeof(arr2)) // 40 = 8 * 5

	// slice 好多人分不清切片和数组的写法区别,其实只要记住[]中间是空的就是切片,反之则是数组即可
	slice1 := []int{1, 2, 3, 4}
	println("slice1:", unsafe.Sizeof(slice1)) // 24

	slice2 := []int{1, 2, 3, 4, 5}
	println("slice2:", unsafe.Sizeof(slice2)) // 24

	slice3 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
	println("slice3:", unsafe.Sizeof(slice3)) // 24
}
1、字符串类型
为什么字符串类型的 unsafe.Sizeof() 一直是16呢?
实际上字符串类型对应一个结构体,该结构体有两个域,第一个域是指向该字符串的指针,第二个域是字符串的长度,每个域占8个字节,但是并不包含指针指向的字符串的内容,这也就是为什么sizeof始终返回的是16。
组成可以理解成此结构体
typedef struct {
    char* buffer;
    size_tlen;
} string;
2、数组类型
编译的时候系统自动分配内存,int的长度是由系统平台来决定的,我用的是64位的系统,所以一个int 代表的就是 int64 数据类型,每个数字占用8个字节,成员数组元素个数正好等于输出的值。byte(1字节),uint8(1字节),uint16(2字节),uint32(4字节),uint64(占用8字节), byte是uint8的别名,这些全是无符号型的,对应的还有有符号型的,区别也就是一个值范围不同而已。

不同数据类型占用字节大小如下:

func main() {
	var a uint8
	a = 2
	fmt.Println("uint8 type size:", unsafe.Sizeof(a))

	var b uint16
	b = 2
	fmt.Println("uint16 type size:", unsafe.Sizeof(b))

	
	var c uint32
	c = 2
	fmt.Println("uint32 type size:", unsafe.Sizeof(c))
	
	var d uint64
	d = 2
	fmt.Println("uint64 type size:", unsafe.Sizeof(d))
}

数据类型

具体类型 取值范围
int8 -128到127
uint8 0到255
int16 -32768到32767
uint16 0到65535
int32 -2147483648到2147483647
uint32 0到4294967295
int64 -9223372036854775808到9223372036854775807
uint64 0到18446744073709551615
3、切片类型
可以看到切片和数组还是有些不一样的,我们看一下官方包的解释 /src/unsafe/unsafe.go
// Sizeof takes an expression x of any type and returns the size in bytes
// of a hypothetical variable v as if v was declared via var v = x.
// The size does not include any memory possibly referenced by x.
// For instance, if x is a slice, Sizeof returns the size of the slice
// descriptor, not the size of the memory referenced by the slice.
意思是说如果是slice的话,则返回的是slice描述符的长度,而不是slice的内存长度。

Golang中的调度器

介绍(Introduction)
———————
Go 1.1最大的特色之一就是这个新的调度器,由Dmitry Vyukov贡献。新调度器让并行的Go程序获得了一个动态的性能增长,针对它我不能再做点更好的工作了,我觉得我还是为它写点什么吧。

这篇博客里面大多数东西都已经被包含在了[原始设计文档](https://docs.google.com/document/d/1TTj4T2JO42uD5ID9e89oa0sLKhJYD0Y_kqxDv3I3XMw)中了,这个文档的内容相当广泛,但是过于技术化了。

关于新调度器,你所需要知道的都在那个设计文档中,但是我这篇博客有图片,所以更加清晰易懂。

带调度器的Go runtime需要什么?(What does the Go runtime need with a scheduler?)
——————————————————————————-
但是在我们开始看新调度器之前,我们需要理解为什么需要调度器。为什么既然操作系统能为我们调度线程了,我们又创造了一个用户空间调度器? Continue reading

[译]Go里面的unsafe包详解

unsafe包位置: src/unsafe/unsafe.go

https://gocn.vip/question/371

指针类型:
*类型:普通指针,用于传递对象地址,不能进行指针运算。
unsafe.Pointer:通用指针,用于转换不同类型的指针,不能进行指针运算。
uintptr:用于指针运算,GC 不把 uintptr 当指针,uintptr 无法持有对象。uintptr 类型的目标会被回收。

unsafe.Pointer 可以和 普通指针 进行相互转换。
unsafe.Pointer 可以和 uintptr 进行相互转换。

也就是说 unsafe.Pointer 是桥梁,可以让任意类型的指针实现相互转换,也可以将任意类型的指针转换为 uintptr 进行指针运算。

一般使用流程:
第一步:将结构体->通用指针unsafe.Pointer(struct)->uintptr(通用指针)获取内存段的起始位置start_pos,并记录下来,第二步使用。
第二步:使用start_pos + unsafe.Offsetof(s.b)->将地址转为能用指针unsafe.Pointer(new_pos)->转为普通指针np = (*byte)(p)->赋值 *np = 20
第三步:打印结构体,发现内容发生了更改。

参考:https://www.cnblogs.com/golove/p/5909968.html

推荐:unsafe.Sizeof() 针对不同数据类型的情况

golang中slice切片理解总结

首先我们对切片有一个大概的理解,先看一下slice的内部结构,共分三部分,一个是指向底层数组的时候,一个是长度len,另一个就是slice的容量cap了。如cap不足以放在新值的时候,会产生新的内存地址申请。

先看代码

package main

import "fmt"

func main() {
	// 创建一个切片,长度为9,容量为10
	fmt.Println("----- 1.测试切片变量append的影响(未申请新的内存空间)-----")
	a := make([]int, 9, 10)
	fmt.Printf("%p len=%d cap=%d %v\n", a, len(a), cap(a), a)

	// 切片进行append操作,由于原来len(a)长度为9,而cap(a)容量为10,未达到扩展内存的要求,此时新创建的切片变量还指向原来的底层数组,只是数组的后面添加一个新值
	// 此时一共两个切片变量,一个是a,另一个是s4。但共指向的一个内存地址
	s4 := append(a, 4)
	fmt.Printf("%p len=%d cap=%d %v\n\n", s4, len(s4), cap(s4), s4)

	// 测试上面提到的切片变量a和s4共指向同一个内存地址, 发现切片数组的第一个值都为7,而唯一不同的是len的长度,而cap仍为10
	fmt.Println("----- 2.测试切片变量共用一个底层数组(内存地址一样)-----")
	a[0] = 7
	fmt.Printf("%p len=%d cap=%d %v\n", a, len(a), cap(a), a)
	fmt.Printf("%p len=%d cap=%d %v\n\n", s4, len(s4), cap(s4), s4)

	// 切片进行append操作后,发现原来的cap(a)的长度已用完了(因为a和s4共用一个底层数组,你也可以理解为cap(s4)),此时系统需要重新申请原cap*2大小的内存空间,所以cap值为10*2=20,将把原来底层数组的值复制到新的内存地址
	// 此时有两个底层数组,一个是切片变量a和s4指向的数组,另一个就是新的切片变量s4
	fmt.Println("----- 3.测试切片变量append的影响(申请了新的内存空间,内存地址不一样了)-----")
	s4 = append(s4, 5)
	fmt.Printf("%p len=%d cap=%d %v\n\n", s4, len(s4), cap(s4), s4)

	// 注意:原切片未发生任何变化,(打印a[0]=7是因为上面第3段落代码已把默认的0值改为了7)
	fmt.Println("----- 4.测试原切片变量a未发生变化-----")
	fmt.Printf("%p len=%d cap=%d %v\n\n", a, len(a), cap(a), a)
}

运行结果:

----- 1.测试切片变量append的影响(未申请新的内存空间)-----
0x10450030 len=9 cap=10 [0 0 0 0 0 0 0 0 0]
0x10450030 len=10 cap=10 [0 0 0 0 0 0 0 0 0 4]

----- 2.测试切片变量共用一个底层数组-----
0x10450030 len=9 cap=10 [7 0 0 0 0 0 0 0 0]
0x10450030 len=10 cap=10 [7 0 0 0 0 0 0 0 0 4]

----- 3.测试切片变量append的影响(申请了新的内存空间)-----
0x10458050 len=11 cap=20 [7 0 0 0 0 0 0 0 0 4 5]

----- 4.测试原切片变量a未发生变化-----
0x10450030 len=9 cap=10 [7 0 0 0 0 0 0 0 0]

在线运行代码:https://play.golang.org/p/pfxZa8T0H1_g

当对slice进行append的时候,会出现以下情况:
1.如果cap超出了原slice的cap长度,则申请原来cap*2的内存空间,再把原来切片所指的底层数组的值复制一份存储到新申请的内存空间里,这样是两块内存地址了,所有原来slice指的底层数组内容没有变化的。此时两个切片,两个底层数组了,每个切片有自己对应的底层数组。
2.如果cap没超出原slice的cap的话,底层对应的数组是没有变化的,但会产生一个新的slice变量,仍然会指向到原来的底层数组(这个新slice变量有自己的内存地址,我们不用关心它,只需要关心他对应的底层数组就可以了)。这时一共两个切片,但共用一个底层数组。

总结:
1.当对slice进行append的时候,如果原slice可以存放下新增加的值,则不会申请新的内存空间,否则会申请cap*2大小基准申请内存空间。而当cap>=1024的时候,会以cap*1.25倍的大小空间基准申请内存空间。
2.每次切片进行append操作,都会返回一个新的切片变量,这个变量有自己的内存地址。 这点开发者知道这点就可以了,一般不用不用关心这点。
3.当切片进行append后,原切片对应的底层数组是不会变化的,这点见第4点。

请参考golang开发源码包 src/runtime/slice.go 中的growslice函数,可以看到对于cap的增长处理逻辑。