Go性能优化 | 青训营笔记

75 阅读7分钟

这是我参与 [第三届青训营 -后端场]笔记创作活动的第6篇笔记

性能优化

what

特定场景下的优化

组件间:不必要的消耗减少

减少io的延迟

why

降低成本

Cycle

业务层优化

语言运行时优化

数据驱动

大瓶颈优先

优化原则

优化保证接口稳定性

测试代码会比优化代码多

日志输出:优化打开了,有收益

自动内存管理

正确性,安全性: 多次释放同一内存,使用以释放的内存

三个任务

相关概念

  • Mutator作用:

  • Collecter: GC线程

  • 算法(GC执行方式)

    • 1
    • 2
    • 3

为什么业务线程,是分配新对象...?

同时执行,挑战..

新增对象标记存活.. 混合写屏障,三色标记,需要的时候才唤起collector,不用就休眠

评价GC算法

  • 安全性(Safety):存活对象不回收
  • 吞吐率(Throughtput):花在GC时间上稍
  • 暂停时间(Pause time):业务是否感知
  • 内存开销(Space overhead):GC元数据开销

两种技术/算法

追踪垃圾回收

  1. 从栈,全局变量标记堆跟对象

  2. 从堆对象找指针指向对象

  3. 清理不可达对象

    • 复制可达对象,其他全删了
    • 标记不可达对象
    • 同一块内存,将可达对象放到一起(指针:top) 原地复制
  4. 不同生命周期(可达/存活)对象,所用的策略不同

  5. 分代GC(为了分类不同生命周期的对象)

    1. 年轻: 函数内对象

      1. 数量少,采用复制年轻代对象,复制开销小,频繁引用开销大(存疑)
      2. 吞吐率高(花点时间占比少) 数量少
    2. 老: 类对象

      1. 使用复制开销大,一直活着,每一次垃圾回收都复制?(标记就好了)
      2. 碎片对象多,那就压缩🗜内存

引用计数

weak reference (解决环形数据结构回收问题)

Go内存管理及优化

Go内存分配

分块 Block

mmap:大块neicun

mspan: 每一列(8kb)

mspan中分为多个(特定大小)块(这里的块就可以放对象了)

缓存 Cache

goruntine:

m

p:

cache 不够用,向 central申请mspan(交换/满的放到mcentral)

Go内存管理优化(字节方案)

  • 小对象多(<80b)

  • 方案:GAB(NoScan <128b)

  • 小对象合并为大对象

  • 为什么小对象可以不等大?

  • 小对象导致大对象存活

    • 将散的对象放到一起(copy GC)

    • 没有Goruntine指向/归属没关系吗?

      • 放到其他位置,散的对象(就没有指针指向他)在下一轮GC中,就被回收了(回收的是原gab的内存(gab是总大小超过阈值,再清理)

        • 问题: 在这一次还有指针指向,如果移动的话,不会造成指针指向未分配内存吗? (什么场景下协程里的许多对象会无指向,而存活少数对象?什么时候会进行垃圾回收?)
        • 修改指针(对象移动,修改指针)
        • noscan:可以理解为书的叶节点
      • 每一个用户的gorountine都有一个gab./像GC这种gorountine,不需要gorountine

      • 对象的大小记录在哪呢?

        • 额外内存空间(0/1:开头)bmap

Go的对象没有对象头/Java的对象有对象头,里面有数据类型等数据标记起始和大小

Survivor Gab:不属于任何内存(有对象活着,就有指针指向这个内存块)

语言顶级会议: paper 论文作者主页 pldi ecrob

mcache:管理各个不同大小mspans

science classes on Go

编译器和静态分析

基本介绍

静态分析: 不执行代码,推导行为,性质

编译器结构/编译流程

数据流和控制流

  • 控制流:分析程序执行的流程
  • 数据流:分析数据在控制流上的传递(结果必为4,则直接返回4即可)

过程内/过程间分析

  • 过程内分析

  • 过程间分析

    • 到底是哪一块内存?(需要同时分析数据流/控制流) 函数内联可避免过多过程间的分析

Go编译器优化(!!!)

背景

Go原生语言目标: 编译时间短,但...

目的:牺牲编译时间换取更高效的机器码

Beast Mode

  • 函数内联
  • 逃逸分析
  • 默认栈大小调整
  • 边界检查消除
  • 循环展开
  • ...

函数内联(Inlining)

内联: 将两个函数合并为一个函数(将被调用函数的函数体(callee)的副本替换到调用位置(caller)上,同时重写代码以反映参数的绑定

真不愧牺牲编译时间

优点:将过程间分析转化为过程内分析(因为都是在同一个内存块内,不需要参数的传递),降低函数调用开销,增加了其他优化机会(逃逸分析)

性能提升: micro-benchmark快速验证性能优化结果(4.58x)

缺点: 函数体变大(instruction cache 复制 不友好),编译生成的Go镜像变大

递归就不内联啦(caller和callee大小策略决定是否内联)

Beast Mode

为什么 interface defer 限制了函数内联?

逃逸分析

概述:分析代码中指针的动态作用域:指针在何处可以被访问

如何理解指针p指向对象逃逸出当前作用域s?

p指向对象可以不通过p指针访问,辣么就可以说p指向对象逃逸出了指针p的作用域s(

作用域:指针p所在代码块(函数,对象)

内联与逃逸分析

若内联一层,则一层间函数调用(callee其实在caller里面)不会造成对象逃逸

优化: 未逃逸的对象可以在栈上分配 栈空间不是很小? 栈不是就存放指针吗? 对象分配不是分配内存吗? 是. 逃逸对象为什么不能在栈上分配

为什么 会分堆和栈? 堆的特点? 栈的特点?

  • 对象在栈上分配和回收很快: 移动sp
  • 减少在heap上的分配,减轻GC负担

Q&A

业务线程和Gorountine

一样的(已知)

GO GC阶段

  • 扫描Goruntine(非全局)

课后作业

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

    1. 业务层:特定场景,提升明显
    2. 运行时层: 通用场景,提升相对不明显
  2. 从软件工程的角度出发, 为了保证语言SDK的可维护性和可拓展性,在进行以运行时优化需要注意什么?

    1. 需要保证向上提供的接口的稳定性
    2. 给用户选择开关
    3. 写好文档,说明作用和说明情况会出问题,做了哪些改动
    4. 运行信息记录到日志中
  3. 自动内存管理技术从大类上分为哪两种,每一种技术的特点以及优缺点?

    1. 引用计数

      1. 用户goruntine即可
      2. 原子化操作,对底层资源请求
    2. 追踪垃圾回收

      1. GC线程
  4. 什么是分代假说? 为了解决什么问题?

    1. 对象按经历垃圾回收次数分为 年轻代/年老代
    2. 为了解决,不同对象的区分(存活周期不同: 全局变量/局部变量)
  5. Go是如何管理和组织内存?

    1. 预先分块(mspan)
    2. 多级缓存
  6. 为什么采用 bump-pointer的方式分配内存会很快?

    1. 因为减少了向mcache申请内存的次数(每一次申请都是大内存)
  7. 为什么我们需要在编译器优化中进行静态分析?

  8. 函数内联 what? 优缺点?

    1. 被调用函数体直接复制到主函数体内, 运行时快(不用函数调用,寄存器,不用拿个指针指来指去)
  9. 逃逸分析 what? 如何提升性能?

    1. 分析对象是否逃离出当前作用域.
    2. 未逃离的对象,可以在栈上分配内存.

\