趁着元旦休假+春节,尝试把2018年期间让我受益的一些文章、问答,翻译一下。
欢迎指正、讨论,希望对你也有所帮助。
原文链接:Go: Are pointers a performance optimization?
以下,开始正文
过去几周时间,我回答了许多关于使用指针优化性能的问题。似乎很多人在这方面都感到困惑。这也可以理解,指针确实是个复杂的话题。 希望这篇文章对你有所帮助。
简而言之:不是使用指针就一定代表着性能优化。
如果要彻底解释这篇文章涉及的所有细节,那篇幅可能会长到没人愿意看。所以,我精简了一下,试图用中等篇幅也能涵盖想说明的高级概念。
阅读时需要说明一点:本文讨论的是微优化,性能优化都是极其细微的。在进行微优化之前,需先进行基准测试,否则很可能看不到明显的效果。代码易读性才是第一要义。
什么是指针?
指针底层代表的就是内存地址,指针解引用可以访问到内存存储的具体数据值。
应用指针之后是如何起到性能优化作用的?
函数调用时,变量传递实际上是将变量重新复制了一份,传给函数。多数情况下,指针都要比变量本身占用更小的空间。
通常,指针大小和系统的架构体系保持一致。32位系统就是32bit,64位系统,即是64bit大小。像bool、float等标量类型,占用的空间都小于等于指针;而多字段的符合类型,指针占的空间更少。
所有,我们的想法就基于复制指针比复制原值更高效。一定程度上,这样想没问题。但是性能问题涉及广泛,除了复制成本之外,还有很多因素要考虑。
指针是否会对性能产生负面影响?
会。主要出于两方面的考量:
- 解引用虽然耗能很小,但积少成多,不得不虑。
- 通过指针共享的数据,是放在堆上的。堆数据的清理是GC负责的,这也会产生开销。随着堆上数据增多,GC的工作量变大,对项目的性能影响也不容忽视。
堆与栈
堆和栈是两个让人头疼的概念,但是我们不得不直面它们。我在这尽量用简短的篇幅讨论完。如果没能快速理解也没关系,我曾经也没能很快理解。
栈:函数局部空间
每当函数被调用,都会分配一块栈空间来存储函数局部变量。函数占用的栈空间大小在编译时已经确定。函数返回时,这块空间就给下一个函数调用使用了,也无需立刻清理。虽然这个分配和使用过程要耗费资源,但相对来讲,消耗很小。
堆:共享数据空间
如上所述,函数返回后,局部变量会被销毁(译者注:空间被复用或者彻底回收,局部变量不再存在)。如果返回是非指针变量,返回值会被复制给调用者,存在于调用者的栈空间中。
但是,如果返回的是指针(译者注:也就是函数局部空间的地址),指针指向的数据就要保存在栈空间之外,这样才能保证函数返回后,数据仍然可以访问。这就是堆的用处。
与堆相关的性能问题有这些:
- 堆空间需要从runtime申请,虽然开销很小,也不能不考虑;
- 如果运行时没有足够空间,就需要系统调用了,这是额外的开销;
- 一旦数据占用了堆空间,就要一直占用到没有指针再指向它。这时候,需要GC来清理。GC会找到堆中所有没有被饮用的值,标记为空闲(译者注:请参考Go垃圾回收的三色标记)。垃圾越多,GC耗时越大,系统性能越差。
那为什么还要用指针?
- 指针能修改传参,提供了一种共享数据的方式。
- 指针能区分零值,确定你的变量是否被赋值了。
总结
指针可以节省复制的开销,但同时要考虑解引用和垃圾回收带来的影响。在我看来,性能分析结果显示复制是瓶颈之前,不应该考虑把指针作为优化方案。计算机在复制方面的速度可是极快的。
希望这篇文章可以让你认识到指针是可以派上用场的,但也不要因为要用而用。
默认还是使用值而不是地址吧。除非语法满足不了你了。