Blog

参与 for range 循环的是 range 表达式的副本

Content #

参与 for range 循环的是 range 表达式的副本 #

我们来看一个简单的例子:

func main() {
    var a = [5]int{1, 2, 3, 4, 5}
    var r [5]int
    fmt.Println("original a =", a)
    for i, v := range a {
        if i == 0 {
            a[1] = 12
            a[2] = 13
        }
        r[i] = v
    }
    fmt.Println("after for range loop, r =", r)
    fmt.Println("after for range loop, a =", a)
}

我们期望这个程序会输出如下结果:

original a = [1 2 3 4 5]
after for range loop, r = [1 12 13 4 5]
after for range loop, a = [1 12 13 4 5]

但实际运行该程序的输出结果却是:

...

循环变量的重用

循环变量的重用 #

for range 形式的循环语句,使用短变量声明的方式来声明循环变量。我们来看下面这个例子:

func main() {
    var m = []int{1, 2, 3, 4, 5}
    for i, v := range m {
        go func() {
            time.Sleep(time.Second * 3)
            fmt.Println(i, v)
        }()
    }
    time.Sleep(time.Second * 10)
}

我们预期的输出结果可能是这样的:

0 1
1 2
2 3
3 4
4 5

而实际输出真的是这样:

4 5
4 5
4 5
4 5
4 5

初学者很可能会被 for range 语句中的短声明变量形式“迷惑”,简单地认为每次迭代都会重新声明两个新的变量 i 和 v。但事实上,这些循环变量在 for range 语句中仅会被声明一次,且在每次迭代中都会被重用。

我们可以将上面的 for range 语句做一个等价转换:

func main() {
    var m = []int{1, 2, 3, 4, 5}
    {
      i, v := 0, 0
        for i, v = range m {
            go func() {
                time.Sleep(time.Second * 3)
                fmt.Println(i, v)
            }()
        }
    }
    time.Sleep(time.Second * 10)
}

我们可以清晰地看到循环变量 i 和 v 在每次迭代时的重用。而 Goroutine 执行的闭包函数引用了它的外层包裹函数中的变量 i、v,这样,变量 i、v 在主 Goroutine 和新启动的 Goroutine 之间实现了共享,而 i, v 值在整个循环过程中是重用的,仅有一份。在 for range 循环结束后,i = 4, v = 5,因此各个 Goroutine 在等待 3 秒后进行输出的时候,输出的是 i, v 的最终值。

...

方法表达式(Method Expression)

方法表达式(Method Expression) #

类型T定义如下:

type T struct {
    a int
}
func (t T) Get() int {
    return t.a
}
func (t *T) Set(a int) int {
    t.a = a
    return t.a
}

Go 语言规范中提供了方法表达式(Method Expression)的概念。有下面代码:

var t T
t.Get()
(&t).Set(1)

我们可以用另一种方式,把上面的方法调用做一个等价替换:

var t T
T.Get(t)
(*T).Set(&t, 1)

这种直接以类型名 T 调用方法的表达方式,被称为 Method Expression。通过 Method Expression 这种形式,类型 T 只能调用 T 的方法集合(Method Set)中的方法,同理类型 *T 也只能调用 *T 的方法集合中的方法。

Method Expression 有些类似于 C++ 中的静态方法(Static Method),C++ 中的静态方法在使用时,以该 C++ 类的某个对象实例作为第一个参数,而 Go 语言的 Method Expression 在使用时,同样以 receiver 参数所代表的类型实例作为第一个参数。

Go 语言中的方法的本质就是,一个以方法的 receiver 参数作为第一个参数的普通函数。而 Method Expression 就是 Go 方法本质的最好体现,因为方法自身的类型就是一个普通函数的类型,我们甚至可以将它作为右值,赋值给一个函数类型的变量,比如下面示例:

...

地址视图

Content #

地址视图的巧妙之处就在于,一个在物理内存上存放的对象,被映射在了三个虚拟地址上。前面我们学习地址映射的时候知道,一个物理地址可以被映射到多个虚拟地址,这个映射方式在同一个进程内同样适用。例如下面的代码:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>

#define PAGE_SIZE 4096
int main() {
    int fd = memfd_create("anonymous", MFD_CLOEXEC);
    ftruncate(fd,PAGE_SIZE);
    char* shm0 = (char*)mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    char* shm1 = (char*)mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    char* shm2 = (char*)mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    sprintf(shm0 ,"hello colored pointer");
    printf("%s\n",shm1);
    printf("%s\n",shm2);
    sprintf(shm1 ,"wow!");
    printf("%s\n",shm0);
    printf("%s\n",shm2);
    close(fd);
    munmap(shm0,PAGE_SIZE);
    munmap(shm1,PAGE_SIZE);
    munmap(shm2,PAGE_SIZE);
    return 0;
}

使用以下命令,编译并执行这个程序:

$ gcc -Wall -D_GNU_SOURCE multi_mmap.c -o multi
$ ./multi

上面的例子先在内存中创建了一个匿名文件,然后将这个匿名文件映射到 shm0, shm1,shm2 三个虚拟地址上。当我们修改 shm0 时,shm1 和 shm2 的内容也会跟着变化。地址视图也是用了同样的原理,三个地址视图映射的是同一块物理内存,映射地址的差异只在第 42-45 位上。这样一个对象可以由三个虚拟地址访问,其访问的内容是相同的。

...

染色指针

染色指针 #

在 64 位系统下,当前 Linux 系统上的地址指针只用到了 48 位,寻址范围也就是 256T。但实际上,当前的应用根本就用不到 256T 内存,也没有哪台服务器机器上面可以一下插这么多内存条。所以, ZGC 就借用了地址的第 42 ~ 45 位作为标记位,第 0 ~ 41 位共 4T 的地址空间留做堆使用。我们结合 JVM 的源码来看看 ZGC 中对地址具体是怎么标注的。 通过上图我们可以看出,第 46 和 47 位是预留的,也就是说标记位可以继续向左移两位,那么可以支持的堆空间就可以扩展到 16T。当前很多资料说 ZGC 只支持 4T 内存,实际上现在最新版本已经支持到了 16T。

第 42-45 这 4 位是标记位,它将地址划分为 Marked0、Marked1、Remapped、 Finalizable 四个地址视图(由于 Finalizable 与弱引用的实现有关系,我们这里只讨论前三个)。

地址视图应该怎么理解呢?其实很简单,对一个对象来说,如果它地址的第 42 位是 1,那么它就被认为是处于 Marked0 视图。依次类推,如果第 43 位是 1,这个对象就处于 Marked1 视图;如果第 44 位是 1,该对象就处于 Remapped 视图。

有了地址视图之后,我们就可以在一个对象转移之后,修改它的地址视图了,同时还可以维护一张映射表(下称 forwarding table)。在这个映射表中,key 是旧地址,value 是新地址。当对象再次被访问时,通过插入的 read barrier 来判断对象是否被搬移过。如果 forwarding table 中有这个对象,说明当前访问的对象已经转移,read barrier 这时就会将对这个对象的引用直接更改为新地址。

...

用读屏障实现并发转移

用读屏障实现并发转移 #

CMS 算法和 G1 算法都使用了 write barrier 来保证并发标记的完整性,防止漏标现象。ZGC 的并发标记也不例外。除此之外,ZGC 提升效率的核心关键在于并发转移阶段使用了 read barrier。

当应用线程去读一个对象时,GC 线程刚好正在搬移这个对象。如果 GC 线程没有搬移完成,那么应用线程可以去读这个对象的旧地址;如果这个对象已经搬移完成,那么可以去读这个对象的新地址。那么判断这个对象是否搬移完成的动作就可以由 read barrier 来完成。 上图中,对象 a 和对象 b 都引用了对象 foo,当 foo 正在拷贝的过程中,应用线程 A 可以访问旧的对象 foo 得到正确的结果,当 foo 拷贝完成之后,应用线程 B 就可以通过 read barrier 来获取对象 foo 的新地址,然后直接访问对象 foo 的新地址。

如果这里只用 write barrier 是否可行?当 foo 正在拷贝的过程中,应用线程 A 如果要写这个对象,那么只能在旧的对象 foo 上写,因为还没有搬移完成;如果当 foo 拷贝完成之后,应用线程 B 再去写对象 foo,是写到 foo 的新地址,还是旧地址呢?

如果写到旧地址,那么对象 foo 就白搬移了,如果写到新地址,那么又和线程 A 看到的内容不一样?所以使用 write barrier 是没有办法解决并发转移过程中,应用线程访问一致性问题,从而无法保证应用线程的正确性。因此,为了实现并发转移,ZGC 使用了 read barrier。

...

指令周期,CPU周期,时钟周期

指令周期,CPU周期,时钟周期 #

计算机每执行一条指令的过程,可以分解成这样几个步骤。

  1. Fetch(取得指令),也就是从 PC 寄存器里找到对应的指令地址,根据指令地址从内存里把具体的指令,加载到指令寄存器中,然后把 PC 寄存器自增,好在未来执行下一条指令。
  2. Decode(指令译码),也就是根据指令寄存器里面的指令,解析成要进行什么样的操作,是 R、I、J 中的哪一种指令,具体要操作哪些寄存器、数据或者内存地址。
  3. Execute(执行指令),也就是实际运行对应的 R、I、J 这些特定的指令,进行算术逻辑操作、数据传输或者直接的地址跳转。
  4. 重复进行 1~3 的步骤。

这样的步骤,其实就是一个永不停歇的“Fetch - Decode - Execute”的循环,我们把这个循环称之为指令周期(Instruction Cycle)。

Machine Cycle,机器周期或者 CPU 周期。CPU 内部的操作速度很快,但是访问内存的速度却要慢很多。每一条指令都需要从内存里面加载而来,所以我们一般把从内存里面读取一条指令的最短时间,称为 CPU 周期。

还有一个是 Clock Cycle,也就是时钟周期以及我们机器的主频。一个 CPU 周期,通常会由几个时钟周期累积起来。一个 CPU 周期的时间,就是这几个 Clock Cycle 的总和。

对于一个指令周期来说,我们取出一条指令,然后执行它,至少需要两个 CPU 周期。取出指令至少需要一个 CPU 周期,执行至少也需要一个 CPU 周期,复杂的指令则需要更多的 CPU 周期。 三个周期(Cycle)之间的关系

所以,我们说一个指令周期,包含多个 CPU 周期,而一个 CPU 周期包含多个时钟周期。

Viewpoint #

From #

17 | 建立数据通路(上):指令+运算=CPU

ImageMagick修剪图片(crop)

Content #

Crop的含义是从原始图片中截取某个矩形。比如:

convert -crop 1104x372+58+100 input.jpg output.jpg

表示要截取的矩形的宽为1104,高为372。矩形的左上角位置在原始图片的 (58, 100)位置。

在处理png图片时,可能需要执行如下操作:

convert +repage a.png b.png

这个操作会将 virtual canvas meta-data 完全重置。

It might be necessary to +repage the image prior to cropping the image to ensure the crop coordinate frame is relocated to the upper-left corner of the visible image. Similarly you may want to use +repage after cropping to remove the page offset that will be left behind. This is especially true when you are going to write to an image format such as PNG that supports an image offset.

...

G1常用参数

G1常用参数 #

G1 的默认参数已经被调整得很好了,大多数情况下,不需要再调整。但是,也不排除特殊情况,因此我们还是需要掌握一些 GC 参数,具体列表如下:

这个表格中最重要、也是你平时最有可能用到的参数,就是 MaxGCPauseMillis。它设置了期望的最大停顿时间。MaxGCPauseMillis 设置的越小,可以控制的停顿时间就越短。但是如果设置得太短,可能会引起 Full GC,代价十分昂贵。

其次,比较关键的参数是 InitiatingHeapOccupancyPercent(IHOP),它的作用是在老年代的内存空间达到一定百分比之后,启动并发标记。当然,这更进一步是为了触发 mixed GC,以此来回收老年代。如果一个应用老年代对象产生速度较快,可以尝试适当调小 IHOP。

Viewpoint #

From #

22 | G1 GC:分区回收算法说的是什么?