Go内存优化&编译器优化思路课后习题 | 青训营笔记

56 阅读4分钟

这是我参与「第五届青训营 」笔记创作活动的第4天。

经过上节课的性能分析实战,我们对影响Go语言性能的一些反面案例有了一定的认识,并且学习使用pprof工具进行了代码性能调优实战。今天老师分享了对Go语言的内存和编译器的优化思路,下面是对课后习题的一个记录。

  1. 从业务层和语言运行时层进行优化分别有什么特点?

    • 业务层优化
      • 针对特定场景,具体问题,具体分析,比如通过工具pprof,发现线上项目存在性能瓶颈,进而进行优化
      • 容易获得较大性能收益
    • 语言运行时优化
      • 解决更通用的性能问题
      • 考虑更多场景
      • Tradeoffs (对于通用性能问题和具体项目性能问题的平衡)
  2. 从软件工程的角度出发,为了保证语言 SDK 的可维护性和可拓展性,在进行运行时优化时需要注意什么?

    • 软件质量至关重要
    • 在保证接口稳定的前提下改进具体实现
      • 因为优化SDK之前已经存在一些稳定的接口以及很多方法依赖该接口,如果我们改变了接口行为,会造成不便
    • 测试用例:覆盖尽可能多的场景,方便回归
    • 文档:做了什么,没做什么,能达到怎样的效果
    • 隔离:通过选项控制是否开启优化
    • 可观测:必要的日志输出
  3. 自动内存管理技术从大类上分为哪两种,每一种技术的特点以及优缺点有哪些?

    • 动态内存
      • 程序在运行时根据需求动态分配的内存:malloc()
      • 优点 :手动管理内存,减少语言运行时对内存的耗时操作
      • 缺点 :容易引发内存操作错误
    • 自动内存管理 (垃圾回收 GC):由程序语言的运行时系统管理动态内存
      • 避免手动内存管理,专注于实现业务逻辑
      • 保证内存使用的正确性和安全性:double-free problem,use-after--free problem
  4. 什么是分代假说?分代 GC 的初衷是为了解决什么样的问题?

    • 分代假说
      • most objects die young
        • 对于全局变量,程序中局部临时变量更多,生命周期更短
    • 目的
      • 针对年轻和老年的对象,制定不同的GC策略,降低整体内存管理的开销
  5. Go 是如何管理和组织内存的?

    • 目标:为对象在heap上分配内存
    • 提前将内存分块
      • 调用系统调用mmap()向OS申请一大块内存,例如4MB
      • 先将内存划分成大块,例如8KB,称作mspan
      • 再将大块继续划分成特定大小的小块,用于对象分配
      • noscan mspan:分配不包含指的对象 一 GC不需要扫描
      • scan mspan:分配包含指针的对象 一 GC需要扫描
    • 对象分配:根据对象的大小,选择最合适的块返回
  6. 为什么采用 bump-pointer 的方式分配内存会很快?

    • 无须和其他分配请求互斥
    • 分配动作简单高效
  7. 为什么我们需要在编译器优化中进行静态代码分析?

    • 静态分析
      • 不执行代码,推导程序的行为,分析程序的性质
      • 控制流(Control flow):程序执行的流程
      • 数据流(Data flow):数据在控制流上的传递
    • 通过分析控制流和数据流,我们可以知道更多关于程序的性质(properties),并根据这些性质来优化代码
  8. 函数内联是什么,这项优化的优缺点是什么?

    • 内联:将被调用函数的函数体(callee)的副本替换到调用位置(caller)上,同时重写代码以反映参数的绑定
    • 优点
      • 消除函数调用开销,例如传递参数、保存寄存器等
      • 将过程间分析转化为过程内分析,帮助其他优化,例如逃逸分析
    • 缺点
      • 函数体变大,instruction cache(icache)不友好,如递归调用一个内联函数,函数体会变得很大
      • 编译生成的Go镜像变大
  9. 什么是逃逸分析?逃逸分析是如何提升代码性能的?

    • 逃逸分析:分析代码中指针的动态作用域:指针在何处可以被访问
    • 大致思路
      • 从对象分配处出发,沿着控制流,观察对象的数据流
      • 若发现指针p在当前作用域s:
        • 作为参数传递给其他函数
        • 传递给全局变量
        • 传递给其他的goroutine
        • 传递给已逃逸的指针指向的对象
      • 则指针·指向的对象逃逸出,反之则没有逃逸出
    • Beast mode:函数内联拓展了函数边界,更多对象不逃逸
    • 优化:末逃逸的对象可以在栈上分配
      • 对象在栈上分配和回收很快:移动sp
      • 减少在heap上的分配,降低GC负担