研究!rsc公司

关于编程的想法和链接,通过

RSS公司

Go数据结构
发布于2009年11月24日星期二。

在向新程序员解释Go时,我发现它通常有助于解释内存中的Go值是什么样子,从而建立正确的直觉,判断哪些操作昂贵,哪些操作不昂贵。这篇文章是关于基本类型、结构、数组和切片的。

基本类型

让我们从一些简单的例子开始:

变量具有类型整数,在内存中表示为单个32位字。(所有这些图片都显示了32位内存布局;在当前的实现中,只有指针在64位机器上变大-整数仍然是32位,尽管实现可以选择使用64位。)

变量j个具有类型整数32,因为显式转换。尽管j个具有相同的内存布局,它们具有不同的类型:赋值i=j是一个类型错误,必须使用显式转换写入:i=整数(j).

变量(f)具有类型浮动,当前实现表示为32位浮点值。它的内存占用与整数32但内部布局不同。

结构和指针

现在情况开始好转。变量字节具有类型[5] 字节,一个5的数组字节它的内存表示就是那5个字节,一个接一个,就像C数组。同样,素数是4的数组整数第条。

Go与C一样,但与Java不同,它让程序员控制什么是指针,什么不是指针。例如,此类型定义:

类型Point结构{X,Y int}

定义名为的简单结构类型,表示为两个相邻整数内存中的。

这个复合文字语法 点{10,20}表示已初始化。获取复合文本的地址表示指向新分配和初始化的前者是记忆中的两个单词;后者是指向内存中两个单词的指针。

结构中的字段在内存中并排排列。

类型Rect1结构{Min,Max Point}类型Rect2结构{Min,Max*Point}

矩形1,一个包含两个字段,由两个s-四个整数-一行。Rect2型,一个包含两个*点字段,由两个*点第条。

使用C语言的程序员可能不会惊讶于字段和*点字段,而只使用过Java或Python(或…)的程序员可能会惊讶于必须做出决定。通过让程序员控制基本内存布局,Go提供了控制给定数据结构集合的总大小、分配数量和内存访问模式的能力,所有这些对于构建性能良好的系统都很重要。

有了这些预备知识,我们可以转向更有趣的数据类型。

(灰色箭头表示实现中存在但在程序中不直接可见的指针。)

A类一串在内存中表示为包含指向字符串数据的指针和长度的2字结构。因为一串是不可变的,多个字符串共享同一存储是安全的,因此切片 结果是一个新的2字结构,其指针和长度可能不同,但仍指向相同的字节序列。这意味着切片可以在不进行分配或复制的情况下完成,使得字符串切片与传递显式索引一样高效。

(作为旁白,有一个著名的gotcha在Java和其他语言中,当您分割字符串以保存一小段时,对原始字符串的引用会将整个原始字符串保留在内存中,即使只需要少量字符串。Go也有这个。我们尝试了另一种选择并被拒绝是为了使字符串切片变得如此昂贵——分配和复制是大多数程序都避免的。)

A类是对数组的一个部分的引用。在内存中,它是一个3字结构,包含指向第一个元素的指针、切片的长度和容量。长度是索引操作的上限,如x(i)而容量是切片操作的上限,如x[i:j].

与对字符串进行切片一样,对数组进行切片也不会产生副本:它只会创建一个新结构,其中包含不同的指针、长度和容量。在示例中,评估复合文字[]整数{2,3,5,7,11}创建一个包含五个值的新数组,然后设置切片的字段x个来描述该数组。切片表达式x[1:3]不分配更多数据:它只写一个新切片结构的字段来引用同一个后备存储。在该示例中,长度为2-年[0]y[1]是唯一有效的索引-但容量是4-年[0:4]是有效的切片表达式。(请参见有效Go有关长度和容量以及如何使用切片的更多信息。)

因为切片是多字结构,而不是指针,所以切片操作不需要分配内存,甚至不需要为切片头分配内存,因为切片头通常可以保存在堆栈中。这种表示方式使得切片的使用成本与在C中传递显式指针和长度对一样低。Go最初将切片表示为指向上述结构的指针,但这样做意味着每个切片操作都会分配一个新的内存对象。即使使用快速分配器,也会给垃圾收集器带来很多不必要的工作,我们发现,与上面的字符串一样,程序避免了切片操作,而更喜欢传递显式索引。删除间接寻址和分配使得切片的成本足够低,可以避免在大多数情况下传递显式索引。

新建和制作

Go有两个数据结构创建功能:新的制作。这种区别是常见的早期混淆点,但似乎很快就会变得自然。基本区别是新(T)返回一个*T型,Go程序可以隐式取消引用的指针(图中的黑色指针),而品牌(T,参数)返回一个普通T型,而不是指针。通常情况下T型其中包含一些隐式指针(图中的灰色指针)。新建返回指向零内存的指针,而制作返回复杂结构。

有一种方法可以统一这两者,但这将是对C和C++传统的重大突破:定义品牌(*T)返回指向新分配的指针T型,因此电流新建(点)将被写入make(*点)。我们尝试了几天,但认为它与人们对分配函数的期望相差太大。

马上就来。。。

这已经有点长了。界面值、地图和频道将不得不等待未来的帖子。

(评论最初通过Blogger发布。)

  • 蒂瓦杰 (2009年11月24日下午12:57)那么与C、C++相比,Go的性能如何呢;Java还是C#?预计降解约-5%?

  • 杜布黑德 (2009年11月24日下午6:22)谢谢你的文章,它重新安排了我对这个主题的理解。

    两种可能的修正:

    在“Slices”中,“x[0:4]是有效的切片表达式”应该类似于“如果y:=x[1:3],那么y[0:4]是有效的片段表达式”。

    在“新建并生成”图中,new(Rect)应该是new(Rect1)。

  • 阿特约姆·沙尔卡科夫 (2009年11月24日下午7:28)您已经展示了Go中的类型规定了数据的内存布局。

    我想知道是否可以使用Go类型系统来描述某些不变量?

  • 塔夫 (2009年11月24日下午7:49) 此帖子已被作者删除。

  • 塔夫 (2009年11月24日下午7:51)嘿Russ,

    谢谢你的这篇内容丰富的文章。作为即将加入Go的Python程序员,我想知道您是否会考虑在未来的文章中使用一系列“最佳Go模式”?

    我——可能还有很多其他人——在内存分配、优化等问题上相当无知。因此,通过比较Go中解决同一问题的“好”和“坏”方法,学习更好的方法将非常有帮助…

    无论如何,我期待着您关于地图、界面和频道的后续帖子。谢谢!

    -- tav@espians.com

  • 朱利安·莫里森 (2009年11月25日2:07 AM)不允许索引超出切片,但允许新切片达到容量,这有什么用处?它是要给出一些近似于Java的Buffer接口的填充指针的东西吗?

  • 俄罗斯考克斯 (2009年11月29日10:07 PM)@蒂瓦吉:这当然是目标。你可以在枪战网站上查看真实的对比。请注意,在这些基准测试中,C或C++通常会有大量的asm。

    @都柏林:谢谢;固定的。

    @Artyom:你想知道什么样的不变量?

    @tav:已经有一个文档尝试这样做:http://golang.org/doc/effective_go.html。

    @朱利安:见http://golang.org/doc/effective_go.html。
    它有助于捕获要禁止的边界外错误
    索引超过len,因为在大多数情况下
    len后面的数据不包含任何有用的内容。

  • 阿特约姆·沙尔卡科夫 (2009年12月3日11:35 PM)@rsc:例如,我想让我的编译器为我检查一个数组的out-of-bounds访问。有一些研究原型(例如ATS),但如果能用实际的编程语言实现这一点就太棒了

  • MikeZ公司 (2009年12月5日上午7:00)关于字符串垃圾收集的问题。假设我们有一个字符串和它的一部分:

    s:=“hello”//ptr到“hello“
    t:=s[2:2]//ptr到“llo”
    s=“再见”//“你好”现在是垃圾吗?

    如果当前垃圾收集器在最后一次分配给s之后收集,t会发生什么情况?当前GC是否足够智能,能够识别t指向“hello”的内部,因此不应该收集“hello“?

  • 布赖恩·布尔科夫斯基 (2009年12月6日下午4:06)我认为“枪战”网站对于我使用语言的目的来说是一个糟糕的衡量标准。我希望能自由使用字符串、散列和列表;这些测试避免了这些结构,并将重点放在循环和读写上。

    话虽如此,枪战声称围棋与二郎处于同一水平,与C相去甚远。

    我看了几个C程序;没有包含asm()语句。Go中的二叉树功能比C慢20倍。

    我愿意相信围棋是不成熟的,需要努力,但当它比C慢20倍(而不是20%)时,听到人们说“围棋就是达到目标”让我感到兴奋。

  • 俄罗斯考克斯 (2009年12月6日下午4:51)@布莱恩:

    二叉树shootout不是一个二叉树基准测试。它是一个垃圾收集基准测试,Go的垃圾收集器速度非常慢。另一方面,它有一个,不像C,它会变得更好。

    如果你看一下实际的计算,而不是库,Go是用等效的C代码保持自己的。反向补码现在几乎与等效的C程序捆绑在一起(还没有在网站上,但在Go存储库中),mandelbrot慢了10%,nbody慢了50%,只是因为它没有内联对sqrt的调用。

    还有,谁说围棋达到了它的表现目标?我认为对于一个粗略的实现来说,它做得很好,但仍有很大的改进空间和许多低垂的成果。

  • 格雷格 (2011年4月4日11:19 PM)非常棒的描述,谢谢Russ。