高性能Go语言发行版优化和落地实践(课后作业+疑问记录)| 青训营笔记

77 阅读6分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记

课后作业

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

  • 业务层优化

    • 针对特定场景,具体问题,具体分析
    • 容易获得较大性能收益
  • 语言运行时优化

    • 解决更通用的性能问题
    • 考虑更多场景
    • Tradeoffs

2.从软件工程的角度出发,为了保证语言SDK的可维护性和可拓展性,在进行运行时优化时需要注意什么?

  • 软件质量至关重要
  • 在保证接口稳定的前提下改进具体实现
  • 测试用例:覆盖尽可能多的场景,方便回归
  • 文档:做了什么,没做什么,能达到怎样的效果
  • 隔离:通过选项控制是否开启优化
  • 可观测:必要的日志输出

3.自动内存管理技术从大类上分为哪两种,每一种技术的特点以及优缺点有哪些?

  • 追踪垃圾回收

    • 对象被回收的条件:指针指向关系不可达的对象

    • 标记根对象

      • 静态变量、全局变量、常量、线性栈等
    • 标记:找到可达对象

      • 求指针指向关系的传递闭包:从根对象出发,找到所有可达对象
    • 清理:所有不可达对象

      • 将存活对象复制到另外的内存空间
      • 将死亡对象的内存标记为可分配:使用free list管理空闲内存
      • 移动并整理存活对象:原地整理对象
    • 根据对象的生命周期,使用不同的标记和清理策略

  • 引用计数

    • 每个对象都有一个与之关联的引用数目

    • 对象存活的条件:当且仅当引用数大于0

    • 优点

      • 内存管理的操作被平摊到程序执行过程中
      • 内存管理不需要了解runtime的实现细节:c++之智能指针
    • 缺点

      • 维护引用计数的开销较大:通过原子操作保证对技术操作的原子性和可见性
      • 无法回收环形数据结构-weak reference
      • 内存开销:每个对象都引入的额外内存空间存储引用数目
      • 回收内存时依然可能引发暂停

4.什么是分代假说?分代 GC 的初衷是为了解决什么样的问题?

  • 分代假说: most objects die young
  • 根据对象的生命周期,使用不同的标记和清理策略

5.Go 是如何管理和组织内存的?

  • 分块

  • 缓存

    Go 使用 TCMalloc 风格的内存管理方式。

    a. TC 是 thread caching 的简写。每个线程都绑定 cache 方便线程快速分配内存;

    b. 内存被划分为特定大小的块。根据对象是否包含指针,将内存块分为 scan 和 noscan 两种;

    c. 根据内存分配请求的大小,选择合适的内存块返回,完成一次内存分配操作;

    d. 回收的内存不会立刻还给操作系统,而是在 Go 内部缓存起来,方便下次分配。

6.为什么采用 bump-pointer 的方式分配内存会很快?

  • 无须和其他分配请求互斥
  • 分配动作简单高效

每个线程都持有用于对象分配的 buffer,因此指针碰撞方式的内存分配无需加锁或使用 CAS 操作;对象分配的操作非常简单。

7.为什么我们需要在编译器优化中进行静态代码分析?

  • 不执行程序代码,推导程序的行为,分析程序的性质
  • 通过分析控制流和数据流,可以知道更多关于程序的性质,并根据这些性质优化代码

8.函数内联是什么,这项优化的优缺点是什么?

  • 内联:将被调用函数的函数体的副本替换到调用位置上,同时重写代码以反映参数的绑定

  • 优点

    • 消除函数调用开销,例如传递参数、保存寄存器等
    • 将过程间分析转化为过程内分析,帮助其他优化,例如逃逸分析
  • 缺点

    • 函数体变大,instruction cache不友好
    • 编译生成的Go镜像变大

9.什么是逃逸分析?逃逸分析是如何提升代码性能的?

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

  • 优化:未逃逸的对象可以在栈上分配

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

疑问记录

The GC runs concurrently with Mutator threads

Concurrently是什么意思?

  • mutator(s)和collector(s)可以同时执行

Mutator threads指的是什么?

  • 业务线程,分配新对象,修改对象指向关系

Buffer?

每个g都绑定一大块内存(1kB),称作 goroutine allcation buffer(GAB)

CAS?

Compare And Swap,比较并交换通常指的是这样一种原子操作:针对一个变量,首先比较它的内存值与某个期望值是否相同,如果相同,就给它赋一个新值。CAS 的伪代码的逻辑:

if (value == expectedValue) {
    value = newValue; }

CAS 可以看作是它们合并后的整体一个不可分割的原子操作,并且其原子性是直接在硬件层面得到保障的。 CAS可以看做是乐观锁的一种实现方式,Java原子类中的递增操作就通过CAS自旋实现的。CAS是一种无锁算法,在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。

golang 可以查看 sync\atomic

CAS 在Golang中是以共享内存的方式来实现的一种同步机制,它是一个原子操作,一般格式如下

fun addValue(delta int32){
​
    for{
​
        oldValue := atomic.LoadInt32(&addr)
​
        if atomic.CompareAndSwapInt32(&addr, oldValue, oldValue+delta){
​
            break;
​
        }
​
    }
​
}

先从一个内存地址 &addr 读取出来当前存储的值,假如读取完以后,没有其它线程对此变量 进行修改的话,则下面的 atomic.CompareAndSwapInt32 语句会在执行时先再判断一次它的值是否与上次的相等,这时必须是相等的,则直接更新它的值;如果在读取值后,有其它线程对变量值进行了修改,发现值不相等,这时就再重新开始下一轮的判断,直到修改成功为止。

bump pointer?

(JVM) 由于Eden区是连续的,因此bump-the-pointer技术的核心就是跟踪最后创建的一个对象,在对象创建时,只需要检查最后一个对象后面是否有足够的内存即可,从而大大加快内存分配速度

cpp 智能指针?

  • 智能指针是一个组合类,旨在管理动态分配的内存并确保在智能指针对象超出范围时删除内存。
  • 这个类持有并“拥有”一个传递给它的指针,然后在类对象超出范围时释放该指针。只要该类的对象仅作为局部变量创建,就可以保证该类将正确地超出范围(无论函数何时或如何终止)并且拥有的指针将被销毁。
  • 如何实现

    • 包含析构函数。当类的对象超出范围时,这些析构函数会自动执行
    • 如果在构造函数中分配(或获取)内存,则可以在析构函数中释放它,并保证在销毁类对象时释放内存(无论它是否超出范围,是否被显式删除…)。