惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
雷峰网
雷峰网
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Spread Privacy
Spread Privacy
H
Hacker News: Front Page
PCI Perspectives
PCI Perspectives
Webroot Blog
Webroot Blog
罗磊的独立博客
H
Heimdal Security Blog
TaoSecurity Blog
TaoSecurity Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
大猫的无限游戏
大猫的无限游戏
月光博客
月光博客
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Google Online Security Blog
Google Online Security Blog
Last Week in AI
Last Week in AI
美团技术团队
Help Net Security
Help Net Security
The Hacker News
The Hacker News
C
Cisco Blogs
T
The Blog of Author Tim Ferriss
J
Java Code Geeks
The Register - Security
The Register - Security
IT之家
IT之家
WordPress大学
WordPress大学
Jina AI
Jina AI
Recent Commits to openclaw:main
Recent Commits to openclaw:main
H
Help Net Security
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
T
Threat Research - Cisco Blogs
P
Proofpoint News Feed
NISL@THU
NISL@THU
爱范儿
爱范儿
The GitHub Blog
The GitHub Blog
Scott Helme
Scott Helme
V
Vulnerabilities – Threatpost
B
Blog
T
Tenable Blog
博客园 - 三生石上(FineUI控件)
T
The Exploit Database - CXSecurity.com
S
Security Affairs
小众软件
小众软件
Hacker News: Ask HN
Hacker News: Ask HN
Security Latest
Security Latest
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
W
WeLiveSecurity
A
Arctic Wolf
L
LINUX DO - 热门话题
Google DeepMind News
Google DeepMind News
M
MIT News - Artificial intelligence

重归混沌的BLOG

给silly实现了一个ernro模块 | 重归混沌的BLOG 给silly实现了一个ernro模块 | 重归混沌的BLOG 第一次在生产环境使用 Vibe Coding | 重归混沌的BLOG 第一次在生产环境使用 Vibe Coding | 重归混沌的BLOG API 设计的艰难抉择 | 重归混沌的BLOG API 设计的艰难抉择 | 重归混沌的BLOG 十年 | 重归混沌的BLOG 十年 | 重归混沌的BLOG 在Go语言中如何使XML加载内存无限趋近于0 | 重归混沌的BLOG 在Go语言中如何使XML加载内存无限趋近于0 | 重归混沌的BLOG 对跨服玩法中的分布式一致性问题进行简单抽象 | 重归混沌的BLOG 对跨服玩法中的分布式一致性问题进行简单抽象 | 重归混沌的BLOG Go语言逃逸分析之slice和map | 重归混沌的BLOG Go语言逃逸分析之slice和map | 重归混沌的BLOG 谈谈观测 | 重归混沌的BLOG 谈谈观测 | 重归混沌的BLOG 写了个AI Agent服务端 | 重归混沌的BLOG 写了个AI Agent服务端 | 重归混沌的BLOG 谈谈代码设计中“严丝合缝” | 重归混沌的BLOG 谈谈代码设计中“严丝合缝” | 重归混沌的BLOG 一次艰难的线上游戏服务器内存排查经历 | 重归混沌的BLOG 一次艰难的线上游戏服务器内存排查经历 如何基于LanguageServerProtocol来编写lint工具 谈谈游戏服务器中RPC模块的设计 谈谈游戏服务器代码抽象 最近碰到的一个分布式一致性问题 谈谈游戏服务器的自动化测试 对Raft协议的一点理解 使用mmap来学习/proc/pid/smaps 2023(完) 再次实现了一个Lua性能分析器 终于给Silly的定时器增加了取消功能 一次虚拟内存排查经历 游戏服务器分布式数据的一种同步的思路 为silly增加了互斥锁 2022(完) Go语言之闭包篇 一例误用unsafe包引起的内存问题 初识Go语言 重新抽象图形API 给Lua实现了一个数学库 谈谈跨平台图形API的抽象 寻路和Flocking算法的结合 行为树的一种高效实现 内测过程中Shader出现的问题 彻底解决多国语言 谈谈数据库的选型 再谈Lua热更新(终) 初窥Rust 关于游戏服务器的服务拆分 ECS的初步实现 ECS初探 屏幕空间(SreenSpace)的想象力 一些对辐射度量学的理解 深度缓冲和半透明渲染 Mysql的间隙锁 更新一些GPU相关知识 2020 地形渲染之爬过的坑 Lua5.3 GC源码阅读(5) 实现一个数据库存储队列 再学计算机图形学入门 再谈分布式服务架构 游戏上线一个月后的反思 一次并发Bug 双向链表的三种实现 谈谈随机数的使用 再谈性能优化 2019 Lua中的函数式编程 重构登录逻辑 Unity资源管理(续) 谈谈Unity的资源管理 一次关于Cache的性能分析 历史之2018 DC3算法 移动平台native代码遭遇的坑 从CPU层面谈谈优化 开卷有益(UNIX编程艺术篇) GC竞争问题 通过Mesh投影来实现贴花系统 谈谈我对数据同步的理解 又一个类型提升引起的Bug Lua5.3 GC源码阅读(4) Lua5.3 GC源码阅读(3) Lua5.3 GC源码阅读(2) Lua5.3 GC源码阅读(1) 三角形光栅化时遇到的坑 一次git事故 再见2017 又一个lua调试器 客户端缓存落地方案 Paxos算法 HTTP服务器的特点 一次性能优化经历 关于CPU分支预测 C程序中让两个不同版本的库共存 实现了一个AOI模块 一个高可伸缩的游戏服务器架构 关于网络协议封装的一些新想法
Go语言之内存篇
重归混沌 · 2022-10-30 · via 重归混沌的BLOG

TL;DR:本文不讨论三色垃圾回收,不讨论读写屏障,不讨论内存分配策略。仅仅从内存视角抽象出一个简单的屏障。以便可以在写Go语言时,知道语言的边界,可以把之前C/C++的经验复用。

上一篇文章中,我提到了一个疑问,就是两个Slice分别引用一个Array的不同部分,GC是如何保证在Mark时,可以Mark到那个被引用的Array。

在这里,我陷入了一个很大的误区。

根据Lua和C#的经验,GC在Mark一个对象时,实际上是Mark一块内存,当这个内存被Mark之后,他就不会被释放。从malloc这个函数也很容易知道,释放一个内存块同样需要内存块的首地址。

这也是为什么很多带GC的语言都不允许做指针运算的原因。

我当时看过的Go语言书籍都说,Go语言虽然有指针,但是不允许做指针运算。

经验主义让我认为,GC系统的主流设计思想都差不多,无非就是算法的不同。

然后,我就有了一种Go语言的指针和C#的引用其实是一个东西的错觉

然而,这种错觉无法解释上一篇文章中有关Slice的GC问题。

事实上,由于潜意识的限制,我甚至忽略了一种更为普遍的情况。

来看一段代码(只是为了演示问题,因为这么做毫无道理):

func foo() *int {
    a := make([]int, 3)
    return &a[1]
}

是的,我甚至弄错了,Go语言的指针是真的指针这一事实。

Go不能做指针运算,指的是我们不能将一个指针加上或减去任意一个偏移量。

Go的指针可以是指向任意一块合法内存的地址。

以上面的代码为例。

当一个函数bar调用foo之后并持有这个int指针,即使Slice变量a被销毁,a所指向的Array也不会被回收。

那么我之前对Go的GC理解必然是错的。

几经辗转,终于在《Go语言设计与实现》中的7.1节“内存分配器的实现原理”找到线索。

Go的内存分配器在1.11版本前后实现是不一样的,《Go语言设计与实现》花了大量笔墨来介绍1.11版本之后的实现细节。

两个版本对上层的抽象是一致的,但是1.11之后的版本稍嫌复杂了,1.11版之前的“线性分配器”版本,更能帮助我建立简单直观的印象。

于是,我找到另一篇文章,这篇文章详细介绍了"线性分配器"的设计思路。

这篇文章中,我们可以得到几个很重要的提示:

  • 内存分配的最小单位是Page
  • 分配出去的内存块是一个称之为mspan的结构,每一个mpan结构一定持整数个Page
  • 任意一个Page都会有与之对应mspan结构的指针,当一个mspan持有多个Page时,多个Page会有相同的mspan结构。

上面提示,已经足够解释前面所有的问题了。

由于每个Page都是同样大小,可以根据内存地址以O(1)的时间复杂度得到Page的索引。

再根据Page的索引,以O(1)的时间复杂度得到mspan的指针。

一个mspan内存块中,所有对象都占用同样大小的内存,使用spanClass来表示对象的大小(spanClass==0例外)。

这样,再根据从mspan得到的对象大小信息,算出指针指向对象的首地址在何处。

当我搞明白这种思路之后,简直都惊呆了。

Go语言通过将内存分配器和GC系统融合之后,提供了几乎90%的指针功能,此时我有点明白“云时代的C语言”这种说法了。


上一篇文章中我炫技似的留下了一段关于接口相关的代码,如下:

package main
import "fmt"
type FooBar interface {
    foo()
    bar()
}
type st1 struct {
    FooBar
    n int
}
type st2 struct {
    FooBar
    m int
}

func (s *st1) foo() {
    fmt.Println("st1.foo", s.n)
}
func (s *st1) bar() {
    fmt.Println("st1.bar", s.n)
}
func (s *st2) foo() {
    fmt.Println("st2.foo", s.m)
}
func test(fb FooBar) {
    fb.foo()
    fb.bar()
}
func main() {
    v1 := &st1{n: 1}
    v3 := &st2{
        m:      3,
        FooBar: v1,
    }
    test(v1)
    test(v3)
}

当时,由于Plan9汇编的阻碍,我对于底层的实现和机制没太明白,更没有明白这种用法的边界是什么。

最近终于有一个自洽的推测了。

是的,因为我目前为止依然看不太懂Plan9汇编,以下全是推测,只有部分佐证。

我先尝试使用C语言写出上面代码的等价代码。

//a.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef void (*foo_t)(void *);
typedef void (*bar_t)(void *);
struct FooBarFn {
    foo_t foo;
    bar_t bar;
};
struct FooBar {
    void *data;
    struct FooBarFn *itab;
};
struct st1 {
    struct FooBar _foobar;
    int n;
};
struct st2 {
    struct FooBar _foobar;
    int m;
};
void st1_foo(struct st1 *s) {
    printf("st1.foo:%d\n", s->n);
}
void st1_bar(struct st1 *s) {
    printf("st1.bar:%d\n", s->n);
}
void st2_foo(struct st2 *s) {
    printf("st2.foo:%d\n", s->m);
}
void st2_bar(struct st2 *s) {
    s->_foobar.itab->bar(s->_foobar.data);
}

struct FooBar st1_interface(struct st1 *s) {
    struct FooBar i;
    i.data = (void *)s;
    i.itab = malloc(sizeof(struct FooBarFn));
    i.itab->foo = (foo_t)st1_foo;
    i.itab->bar = (bar_t)st1_bar;
    return i;
}

struct FooBar st2_interface(struct st2 *s) {
    struct FooBar i;
    i.data = (void *)s;
    i.itab = malloc(sizeof(struct FooBarFn));
    i.itab->foo = (foo_t)st2_foo;
    i.itab->bar = (bar_t)st2_bar;
    return i;
}

void test(struct FooBar bar) {
    bar.itab->foo(bar.data);
    bar.itab->bar(bar.data);
}
int main() {
    struct FooBar i1, i2;
    struct st1 *v1 = malloc(sizeof(*v1));
    struct st2 *v3 = malloc(sizeof(*v3));
    memset(v1, 0, sizeof(*v1));
    memset(v3, 0, sizeof(*v3));
    v1->n = 1;
    v3->m = 3;
    v3->_foobar = st1_interface(v1);
    i1 = st1_interface(v1);
    i2 = st2_interface(v3);
        test(i1);
    test(i2);
    return 0;
}
//gcc -o a a.c

上面这段代码是可以被编译通过的,而且和各种Go语言书中披露的interface实现,非常接近,我几乎可以认定Go语言就是这么实现的。

这段代码主要想解释“结构/接口内嵌”,编译器到底做了什么,他的规则是什么,以便我可以更好的利用这种规则。

Go的整个嵌入结构其实非常酷炫,但是也难以理解。

但是如果按上面的C代码去分析,其实整个规则非常简单,只是两个语法糖而已。

先来单纯看struct的内存布局。

在C语言时代我们所有人都写过下面这种代码:

struct A {
    int f1;
    int f2;
};
struct B {
    struct A a;
    int f3;
};
void foo() {
    struct B b;
    b.a.f1 = 3;
    b.a.f2 = 4;
    b.f3 = 5;
}

对应的Go语言如下:

type A struct {
    f1 int
    f2 int
}
type B struct {
    A
    f3 int
}
type D struct {
    A a
    f3 int
}
func foo() {
    b := new(B)
    b.f1 = 3
    b.f2 = 4
    b.f3 = 5
    d := new(D)
    d.a.f1 = 3
    d.a.f2 = 4
    d.f3 = 5
}

可以看到,内嵌结构体的字段访问,其实就是个语法糖。

Go编译器在编译阶段, 会将结构B转换为结构D,再进行编译(注:这里是指源码级,由于是值嵌入,在编译时,可以直接算出地址偏移量,在汇编层面优化不优化都没有任何区别,如果是指针嵌入效果又不一样)。

下面让我们来证明一下这个结论:

package main
import (
    "fmt"
    "unsafe"
)
type A struct {
    f1 int8
    f2 int8
}
type B struct {
    A
    f3 int8
}
func (*A) foo() {}
func main() {
    var a A
    var b B
    fmt.Println(unsafe.Sizeof(a))
    fmt.Println(unsafe.Sizeof(b))
}

上面的代码可以证明,关于struct结构布局并没有什么魔法,B结构的大小就是A结构的大小+int8的大小。

同理,type B struct {*A}type B struct {a *A}也并没有任何区别。

再来看函数,当一个B嵌入A时,他就有了A的所有函数, 如foo函数。

其实,这也是一个很甜的语法糖,甜到都像是魔法了。

当B嵌入了A之后,他会帮B生成一套A的所有函数,这样B就有了自己的foo函数。

而B.foo函数的函数体,其实只干一件事,就是再调用A.foo函数。

之所以会这样,是因为调用A.foo时,需要传入A对象的内存地址。

这一切都是优化前的思路。

如果你直接去反汇编,可能会得到不同的结论。

为了少生成一条call指令,编译器通常会在调用B.foo时,直接生成B.A.foo代码。

但是我们可以通过println来找到蛛丝马迹。

func main() {
    fA := (*A).foo
    fB := (*B).foo
    println(fA)
    println(fB)
}

至此,Go语言的所有内存布局相关的细节,我们基本上都和C语言对上了。

ps. 有人说研究这些没有用。但是不搞清语言的边界,怎么才能发挥出一个语言的最大威力呢 ^_^!