朱晓峰

一只生之无趣死之乏味的丧家之犬

0%

Cache Line

数据经过主存、L3、L2、L1 Cache到CPU寄存器进行运算,访问的速度逐级提高,L3一般为多核共享,L2、L1一般为单核本地使用。

按照局部性原理,正在使用的数据的附近数据也大概率会被使用,所以CPU会按照Cache Line为一个单位加载一批连续的数据到缓存里,主流CPU一般为64B

例如你有一个数组,每个元素的类型为int64,大小为8B,当你用到第一个元素时,CPU会将另外7个元素也一起作为一个Cache Line加载到各级缓存中

什么是False sharing伪共享

false sharing

两个线程修改同一条Cache Line上的不同位置,发生了缓存不一致的情况,会导致L1、L2缓存上这条Cache Line被认为是无效数据

此时就需要MESI缓存一致性协议RFO请求核间通信方式,来标记自己本地缓存上Cache Line的状态并通知另一个核是否需要更新缓存,最后通过相对比较慢的L3 Cache同步数据,甚至穿透到主存,总之这些都会带来开销

所以在这种情况下没有真正达到共享的目的,此为伪共享(其实应该翻译为错误共享)

那延伸一下,Cache Line如果大小为1B一个单位,只要不是修改到同一个字节的位置,那是不是就没有这个问题了?

但随之而来的是局部性的损失以及控制电路复杂度的提高,设计就是不断权衡利弊,这是另一个话题了

如何验证伪共享问题呢,我要眼见为实

复现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import (
"fmt"
"time"
)

// a demo for false share
type MyInt64 struct {
val int64 // 8Byte
}
var (
N = 100000000
M = 8
List = new([8]MyInt64)
)
func run(idx int) {
for i:=N; i>0; i-- {
List[idx].val = int64(i)
}
}
func main() {
ch := make(chan int, M) // exit signal
start := time.Now()
for i:=0; i<M; i++ {
go func(i int) {
run(i)
ch <- 1
}(i)
}
for i:=0; i<M; i++ {
<-ch
}
fmt.Printf("%v\n", time.Since(start))
}

我们构造一个长度为8的数组,每个元素都是一个大小为64bit的类型,共占64Byte

然后用8个协程分别更新各自索引位置的元素

执行 go run main.go

此时耗时约在秒级:2.5329389s

改进

如果我们加上一些padding,此时数组中每个元素都是64Byte的类型,每个元素都占满一个Cache,使得每个核操作不同的Cache Line

1
2
3
4
5
type padding int64
type MyInt64 struct {
val int64 // 8Byte
pad1, pad2, pad3, pad4, pad5, pad6, pad7 padding // 56Byte
}

耗时约在百毫秒级别:113.9928ms

总结

所以可见,如果我们编写的程序恰巧遇到了False Sharing,会导致严重的性能问题。

目前perf-c2c等工具可以检测伪共享问题。开源的Disruptor框架也详细阐述了该问题

Golang在sync.Pool中也有对应的设计

1
2
3
4
5
6
7
type poolLocal struct {
poolLocalInternal

// Prevents false sharing on widespread platforms with
// 128 mod (cache line size) = 0 .
pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}

但是真的结束了吗?

如果在上面的代码中不用数组,而是用slice会怎样?

slice底层是什么结构,会有什么影响?

1
2
3
4
5
type slice struct {
array unsafe.Pointer
len int
cap int
}

如果改成8个pad会怎样?哪种对齐的结构对编译器更加友好?

(实际8个pad速度更快,可以试试)

7个pad

1
2
3
4
5
6
7
8
9
10
11
MOVQ	AX, SI
SHLQ $6, AX
MOVQ DX, (BX)(AX*1)
DECQ DX
MOVQ SI, AX
TESTQ DX, DX
JLE 71
MOVQ main.List+8(SB), CX
MOVQ main.List(SB), BX
NOP
CMPQ AX, CX

8个pad

1
2
3
4
5
6
7
8
LEAQ	(AX)(AX*8), SI
MOVQ DX, (BX)(SI*8)
DECQ DX
TESTQ DX, DX
JLE 56
MOVQ main.List(SB), BX
TESTB AL, (BX)
CMPQ AX, $8

如果关闭编译优化会怎样?或者说编译优化究竟在优化什么?

关闭编译优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
go build -gcflags="-N -l -S" main.go
0x000e 00014 (main.go:18) MOVQ AX, main.idx+40(SP)
0x0013 00019 (main.go:19) MOVQ main.N(SB), DX
0x001a 00026 (main.go:19) MOVQ DX, main.i+16(SP)
0x001f 00031 (main.go:19) NOP
0x0020 00032 (main.go:19) JMP 34
0x0022 00034 (main.go:19) CMPQ main.i+16(SP), $0
0x0028 00040 (main.go:19) JGT 44
0x002a 00042 (main.go:19) JMP 91
0x002c 00044 (main.go:20) MOVQ main.idx+40(SP), AX
0x0031 00049 (main.go:20) MOVQ main.List+8(SB), CX
0x0038 00056 (main.go:20) MOVQ main.List(SB), DX
0x003f 00063 (main.go:20) MOVQ main.i+16(SP), BX
0x0044 00068 (main.go:20) CMPQ CX, AX
0x0047 00071 (main.go:20) JHI 75
0x0049 00073 (main.go:20) JMP 101
0x004b 00075 (main.go:20) LEAQ (DX)(AX*8), DX
0x004f 00079 (main.go:20) MOVQ BX, (DX)
0x0052 00082 (main.go:20) JMP 84
0x0054 00084 (main.go:19) DECQ main.i+16(SP)
0x0059 00089 (main.go:19) JMP 34
0x005b 00091 (main.go:22) MOVQ 24(SP), BP
0x0060 00096 (main.go:22) ADDQ $32, SP
0x0064 00100 (main.go:22) RET
0x0065 00101 (main.go:20) PCDATA $1, $0
0x0065 00101 (main.go:20) CALL runtime.panicIndex(SB)
0x006a 00106 (main.go:20) XCHGL AX, AX

开启编译优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go build -gcflags=-S main.go
0x000e 00014 (main.go:19) MOVQ main.N(SB), DX
0x0015 00021 (main.go:19) JMP 32
0x0017 00023 (main.go:20) MOVQ DX, (BX)(AX*8)
0x001b 00027 (main.go:19) DECQ DX
0x001e 00030 (main.go:19) NOP
0x0020 00032 (main.go:19) TESTQ DX, DX
0x0023 00035 (main.go:19) JLE 58
0x0025 00037 (main.go:20) MOVQ main.List+8(SB), CX
0x002c 00044 (main.go:20) MOVQ main.List(SB), BX
0x0033 00051 (main.go:20) CMPQ AX, CX
0x0036 00054 (main.go:20) JCS 23
0x0038 00056 (main.go:20) JMP 68
0x003a 00058 (main.go:22) MOVQ 16(SP), BP
0x003f 00063 (main.go:22) ADDQ $24, SP
0x0043 00067 (main.go:22) RET
0x0044 00068 (main.go:20) PCDATA $1, $0
0x0044 00068 (main.go:20) CALL runtime.panicIndex(SB)
0x0049 00073 (main.go:20) XCHGL AX, AX

可以看到把很多操作压在DX寄存器里操作了,不用内存取值了

前言

记录一下实现一个想法的过程

真实影像

远程天文台拍摄过程

107_psc_deepsky_shooting

原图

处理过程

107_psc_pixinsight

光谱图像制作

获取原始数据

  1. 数据源:https://www.eso.org/sci/observing/phase3/data_streams.html
  2. 搜索HD编号:HD 10476,找到两份光谱数据,选择合适的下载,最好包含可见光波段

107_psc_search_spectrum

处理光谱数据

  1. 读取数据,并转换合适的单位
  2. 降采样(可选)
  3. Savitzky-Golay滤波
  4. 输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import os
from astropy.io import fits
from scipy.signal import savgol_filter
import matplotlib.pyplot as plt

# 导入文件
filepath = os.path.join(r'./spectrum', 'ADP.2016-09-27T07_02_47.049.fits')
f = fits.open(filepath)
spec = f[1]
data = spec.data

# 获取数据,并转换单位
wave_nm = [wvl/10 for wvl in list(data.field(0)[0])]
flux = list(data.field(1)[0])

# 降采样
wave_nm = wave_nm[0::3]
flux = flux[0::3]

# Savitzky-Golay滤波
flux = savgol_filter(flux, 53, 1, mode= 'nearest')

# 设置输出图片尺寸、标题、纵轴、横轴
plt.figure().set_size_inches(24, 4)
plt.title('HD 10476 Spectrum Comparison')
plt.xlabel('Wavelength (nm)')
plt.ylabel('Flux (ADU s-1 mW-1)')

# 绘制
plt.plot(wave_nm, flux)
plt.show()

滤波前的结果

107_psc_specturm_no_filter

滤波后的结果

107_psc_specturm

创建渐变光谱背景

由于我的Adobe Photoshop 2022没有自带的光谱渐变,所以我只能手工创建

色阶并不是均分的,因为在人眼可见光中,红光占的谱线宽度比别的颜色要更宽,所以在编辑颜色的渐变时,给红色到黑色更长的渐变过程

107_psc_PS_gradient_editor

合并图层

107_psc_PS_gradient_merge_layer

用universe sandbox软件做模拟效果图

107_psc_universe_sandbox

最后PS合成

  1. 添加背景
  2. 调整光谱背景的对比度
  3. 将python导出的光谱曲线导入
  4. 增加波长刻度
  5. 增加黑色的竖线,调整透明度
  6. 增加文字,外发光效果

107_psc_PS_all_layers

后记

我发现天文数据虽然很公开,但都是在自己平台发布自己天文设备采集到的数据,导致搜寻数据困难,而且数据格式不一,虽然有很多数据整合平台,但感觉反而更乱了,花了很大精力,开了无数个chrome标签页,爆了无数次内存,才终于找到可见光波段的光谱数据

滤波调参时,有一个重要参考,至少要将钠的D1、D2两条很近的吸收带分辨出来

虽然全球各地有很多远程天文台可以租用,总有夜晚晴朗的天文台,但仍然很容易观测失败,例如天空突然飞来一堆云、对焦失败、赤道仪故障星点拉线等

用恒星光谱作为礼物可能是个还不错的选择,但这不是流水线式的工作,最重要的还是真诚。

panic日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x512f5d]

goroutine 48438156 [running]:
encoding/json.(*encodeState).marshal.func1(0xc009decb40)
/go/src/encoding/json/encode.go:326 +0x9a
panic(0xd41f60, 0x187d170)
/go/src/runtime/panic.go:967 +0x15d
encoding/json.(*encodeState).string(0xc004eba100, 0x0, 0x7, 0x1)
/go/src/encoding/json/encode.go:1003 +0x5d
encoding/json.stringEncoder(0xc004eba100, 0xce54e0, 0xc008bfeea0, 0x198, 0x100)
/go/src/encoding/json/encode.go:644 +0x358
encoding/json.structEncoder.encode(0xc00451c000, 0x13, 0x21, 0xc0058d99e0, 0xc004eba100, 0xe29520, 0xc008bfeea0, 0x199, 0xd30100)
/go/src/encoding/json/encode.go:758 +0x2bb
encoding/json.ptrEncoder.encode(0xc0058d9a70, 0xc004eba100, 0xd26880, 0xc008bfeea0, 0x16, 0xd20100)
/go/src/encoding/json/encode.go:914 +0x116
encoding/json.(*encodeState).reflectValue(0xc004eba100, 0xd26880, 0xc008bfeea0, 0x16, 0xc009de0100)
/go/src/encoding/json/encode.go:358 +0x82
encoding/json.(*encodeState).marshal(0xc004eba100, 0xd26880, 0xc008bfeea0, 0x450100, 0x0, 0x0)
/go/src/encoding/json/encode.go:330 +0xf0
encoding/json.Marshal(0xd26880, 0xc008bfeea0, 0x18cf180, 0x8, 0xc0029067a8, 0xc002038c40, 0x15)
/go/src/encoding/json/encode.go:161 +0x52
...

可以看到,是json encode string的时候发生的panic,这种情况比较少见。

重点是这一行日志:

encoding/json.(*encodeState).string(0xc004eba100, 0x0, 0x7, 0x1)

string()函数的入参有0x0和0x7

实际上这里的0x0是指字符串变量指向内容的指针为0,表示长度的变量为0x7

原理

string类型底层实现是一个指向内容的指针和一个表示长度的int

1
2
3
4
5
6
7
8
9
10
// StringHeader is the runtime representation of a string.
// It cannot be used safely or portably and its representation may
// change in a later release.
// Moreover, the Data field is not sufficient to guarantee the data
// it references will not be garbage collected, so programs must keep
// a separate, correctly typed pointer to the underlying data.
type StringHeader struct {
Data uintptr
Len int
}

对字符串的赋值,分为两部分,所以在go里对字符串进行并发读写的话,有可能会导致指向的内容和长度不匹配,导致一些奇怪的问题。

比如两个协程同时并发读写同一个字符串变量,一个赋值为”123”,另一个赋值为”123456”,该字符串有可能为”123”,但长度是6。

但是就算并发读写,Data指针指向的一直是正常地址,只是长度不一定匹配,又怎么会导致nil pointer的呢?

其实,在go里,有一种特殊优化,如果你把字符串赋值为””,即空字符串的话,Data指针是0,也即空指针。

我们来验证一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package main

import (
"fmt"
"time"
)


type AudioClips struct {
Param *TableStruct
}

type TableStruct struct {
RequestId string
Type string
Organization string
BtId string
AppId string
TokenId string
Channel string
}


func f(org, serviceId, appId, channel string) {
curTime := time.Now().Format(layout)
fmt.Sprintf("%s@%s@%s@%s@%s@%s", curTime, org, serviceId, appId, channel, "length")
}

var layout string

func main() {
a := AudioClips{
Param: &TableStruct{
AppId: "",
Organization: "org",
Channel: "channel",
},
}

go func() {
for {
f(a.Param.Organization, "serviceId", a.Param.AppId, a.Param.Channel)
}
}()

for {
go func() {a.Param.AppId = ""}()
time.Sleep(10*time.Nanosecond)
go func() {a.Param.AppId = "default"}()
time.Sleep(10*time.Nanosecond)
}
}

运行后发生panic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ go run main.go
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x0 pc=0xe570a2]

goroutine 34 [running]:
fmt.(*buffer).writeString(...)
C:/Program Files/Go/src/fmt/print.go:82
fmt.(*fmt).padString(0xc0000202b0, 0x0, 0x7)
C:/Program Files/Go/src/fmt/format.go:110 +0x98
fmt.(*fmt).fmtS(0xc0000202b0, 0x0, 0x7)
C:/Program Files/Go/src/fmt/format.go:359 +0x68
fmt.(*pp).fmtString(0xc000020270, 0x0, 0x7, 0x21700000073)
C:/Program Files/Go/src/fmt/print.go:446 +0x1d2
fmt.(*pp).printArg(0xc000020270, 0xead6c0, 0xc0003de2b0, 0x73)
C:/Program Files/Go/src/fmt/print.go:694 +0x875
fmt.(*pp).doPrintf(0xc000020270, 0xec9c4d, 0x11, 0xc000097f18, 0x6, 0x6)
C:/Program Files/Go/src/fmt/print.go:1026 +0x168
fmt.Sprintf(0xec9c4d, 0x11, 0xc000097f18, 0x6, 0x6, 0x0, 0x0)
C:/Program Files/Go/src/fmt/print.go:219 +0x6d
main.f(0xec799a, 0x3, 0xec8574, 0x9, 0x0, 0x7, 0xec7fd9, 0x7)
C:/Projects/test/11/main.go:43 +0x1de
main.main.func1(0xc000194000)
C:/Projects/test/11/main.go:59 +0x8a
created by main.main
C:/Projects/test/11/main.go:57 +0x91
exit status 2

看下汇编代码,略有精简

go tool compile -S -N -l main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
...
"".main.func2 STEXT size=92 args=0x8 locals=0x8
0x0000 00000 (main.go:47) TEXT "".main.func2(SB), ABIInternal, $8-8
0x0000 00000 (main.go:47) MOVQ (TLS), CX
0x0009 00009 (main.go:47) CMPQ SP, 16(CX)
0x000d 00013 (main.go:47) PCDATA $0, $-2
0x000d 00013 (main.go:47) JLS 85
0x000f 00015 (main.go:47) PCDATA $0, $-1
0x000f 00015 (main.go:47) SUBQ $8, SP
0x0013 00019 (main.go:47) MOVQ BP, (SP)
0x0017 00023 (main.go:47) LEAQ (SP), BP
0x001b 00027 (main.go:47) MOVQ "".a+16(SP), CX
0x0020 00032 (main.go:47) TESTB AL, (CX)
0x0022 00034 (main.go:47) MOVQ $0, 72(CX)
0x002a 00042 (main.go:47) LEAQ 64(CX), DI
0x002e 00046 (main.go:47) CMPL runtime.writeBarrier(SB), $0
0x0035 00053 (main.go:47) JEQ 57
0x0037 00055 (main.go:47) JMP 76
0x0039 00057 (main.go:47) MOVQ $0, 64(CX)
0x0041 00065 (main.go:47) JMP 67
0x0043 00067 (main.go:47) PCDATA $0, $-1
0x0043 00067 (main.go:47) PCDATA $1, $-1
0x0043 00067 (main.go:47) MOVQ (SP), BP
0x0047 00071 (main.go:47) ADDQ $8, SP
0x004b 00075 (main.go:47) RET
...

MOVQ $0, 72(CX) 表示把立即数0存入CX地址+72,即把长度0存入Len

MOVQ $0, 64(CX) 表示把立即数0存入CX地址+64,即指向字符串内容的指针值为0,也就是nil pointer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
...
"".main.func3 STEXT size=100 args=0x8 locals=0x8
0x0000 00000 (main.go:49) TEXT "".main.func3(SB), ABIInternal, $8-8
0x0000 00000 (main.go:49) MOVQ (TLS), CX
0x0009 00009 (main.go:49) CMPQ SP, 16(CX)
0x000d 00013 (main.go:49) PCDATA $0, $-2
0x000d 00013 (main.go:49) JLS 93
0x000f 00015 (main.go:49) PCDATA $0, $-1
0x000f 00015 (main.go:49) SUBQ $8, SP
0x0013 00019 (main.go:49) MOVQ BP, (SP)
0x0017 00023 (main.go:49) LEAQ (SP), BP
0x001b 00027 (main.go:49) MOVQ "".a+16(SP), CX
0x0020 00032 (main.go:49) TESTB AL, (CX)
0x0022 00034 (main.go:49) MOVQ $7, 72(CX)
0x002a 00042 (main.go:49) LEAQ 64(CX), DI
0x002e 00046 (main.go:49) PCDATA $0, $-2
0x002e 00046 (main.go:49) PCDATA $1, $-2
0x002e 00046 (main.go:49) CMPL runtime.writeBarrier(SB), $0
0x0035 00053 (main.go:49) JEQ 57
0x0037 00055 (main.go:49) JMP 79
0x0039 00057 (main.go:49) LEAQ go.string."default"(SB), AX
0x0040 00064 (main.go:49) MOVQ AX, 64(CX)
0x0044 00068 (main.go:49) JMP 70
0x0046 00070 (main.go:49) PCDATA $0, $-1
0x0046 00070 (main.go:49) PCDATA $1, $-1
0x0046 00070 (main.go:49) MOVQ (SP), BP
0x004a 00074 (main.go:49) ADDQ $8, SP
0x004e 00078 (main.go:49) RET
...

MOVQ $7, 72(CX) 表示把立即数7存入CX地址+72,即把长度0存入Len

1
2
LEAQ    go.string."default"(SB), AX
MOVQ AX, 64(CX)

表示把指向”default”的地址存入AX,再把AX存入64(CX),即把指向字符串内容的指针赋值为”default”的地址

所以在json encode这个字符串的时候,可能会发生字符串有长度,但指向内容的指针是空指针,

可以通过len(str)>0这样的判断,但真正去取值的时候就发生了nil pointer panic。

更进一步

所以可见,如果某个字符串变量存储了很多的内容,可以通过赋值为空字符串进行释放。

那么问题又来了,如果按上面的分析,赋值为空字符串,指针就会为空,那为什么如下代码没有发生panic

1
2
a := ""
p := &a

其实编译器会判断到如果这个空字符串在后续是有取指针操作的话,加上如下汇编代码:

1
2
3
4
5
6
7
0x003c 00060 (main.go:9)	MOVQ	"".&p+24(SP), DI
0x0041 00065 (main.go:9) MOVQ "".&a+16(SP), AX
0x0046 00070 (main.go:9) CMPL runtime.writeBarrier(SB), $0
0x004d 00077 (main.go:9) JEQ 81
0x004f 00079 (main.go:9) JMP 95
0x0051 00081 (main.go:9) MOVQ AX, (DI)
0x0054 00084 (main.go:9) JMP 86

以保证可以正常取到指针

项目里经常出现panic: assignment to entry in nil map的错误,问题其实很简单,但第一眼不一定能看出来。

复现demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

import (
"math/rand"
"runtime"
"sync"
"time"
)

type Task struct {
Result string
}

func (t *Task) f(wg *sync.WaitGroup) map[string]interface{} {
defer wg.Done()
// do something
time.Sleep(10*time.Microsecond)
return map[string]interface{}{t.Result: rand.Int()}
}

func run() {
var wg sync.WaitGroup
tasks := []Task{
{Result: "task1"},
{Result: "task2"},
}
result := make([]map[string]interface{}, 2)

for idx, task := range tasks {
wg.Add(1)
go func(i int, t Task) {
result[i] = t.f(&wg)
}(idx, task)
}
wg.Wait()

result[0]["test"] = 0
result[1]["test"] = 1
}

func main() {
runtime.GOMAXPROCS(2)
for i := 0; i < 10000; i++ {
run()
}
}

代码逻辑是期望并发执行两个任务,然后使用得到的结果,但却发生了panic。

原因是在错误的地方使用了defer wg.Done(),内部函数f执行结束后,还没有将返回值赋值给result[i],主线程就继续运行了,进而发生nil map panic。

前人挖的坑,后人的kpi。

Gitlab 14.x相较于13.x删除了unicorn,使用puma。所以需要注释掉gitlab.rb中所有关于unicorn选项,打开puma的相关选项。

提供一份配置示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
################################################################################
## GitLab Unicorn
##! Tweak unicorn settings.
##! Docs: https://docs.gitlab.com/omnibus/settings/unicorn.html
################################################################################

# unicorn['enable'] = true
# unicorn['worker_timeout'] = 60
###! Minimum worker_processes is 2 at this moment
###! See https://gitlab.com/gitlab-org/gitlab-ce/issues/18771
# unicorn['worker_processes'] = 2
# unicorn['worker_memory_limit_min'] = "250 * 1 << 20"
# unicorn['worker_memory_limit_max'] = "400 * 1 << 20"

################################################################################
## GitLab Puma
##! Tweak puma settings. You should only use Unicorn or Puma, not both.
##! Docs: https://docs.gitlab.com/omnibus/settings/puma.html
################################################################################

puma['enable'] = true
# puma['ha'] = false
puma['worker_timeout'] = 300
puma['worker_processes'] = 4
puma['min_threads'] = 1
puma['max_threads'] = 16

背景

偶然发现项目代码中有如下的写法:

1
2
3
4
5
6
7
8
type Student struct {
Name string
Age int
}

func NewStudent(s Student) *Student {
return &s
}

我们且不去探讨“这样写法有什么作用”、“为什么要这样写”,我们先就代码本身讨论。

众所周知,从严格意义上来说,golang函数调用中只有值传递的方式传递参数,所以这个函数会返回一个新的指针,并指向一个新的对象。

然后我就在思考,这种函数会被编译器特殊优化么?

探索

写一个简单demo测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

type A struct{} // 空struct

func NewStructA(a A) *A {
return &a
}

func main() {
a := A{}
newA := NewStructA(a)
fmt.Printf("a: %p, newA: %p\n", &a, newA)
}

运行结果很amazing啊

1
2
$ go run main.go
a: 0x5a6d88, newA: 0x5a6d88

两个指针居然完全相等,这不合常理啊,讲道理,值传递的话,return返回出来的应该是一个新对象的指针啊

不过仔细观察一下,这个例子中,A是一个空struct,函数里也不可能修改传进来的A的实例a,所以即使两个是相同的指针也没有任何影响。

那我们就去看看编译器是怎么做的吧。

-gcflags='-S' 列出汇编代码

FUNCDATAPCDATA是编译器产生的,用于保存一些给垃圾收集的信息,在本例中可以过滤掉

1
2
3
4
5
6
7
8
9
10
$ go build -gcflags='-S' main.go 2>&1 | grep -v "FUNCDATA\|PCDATA" | grep -A10 '"".NewStructA STEXT'

"".NewStructA STEXT nosplit size=13 args=0x8 locals=0x0
0x0000 00000 (D:\projects\test\go\struct\1\main.go:7) TEXT "".NewStructA(SB), NOSPLIT|ABIInternal, $0-8
0x0000 00000 (D:\projects\test\go\struct\1\main.go:8) LEAQ runtime.zerobase(SB), AX
0x0007 00007 (D:\projects\test\go\struct\1\main.go:8) MOVQ AX, "".~r1+8(SP)
0x000c 00012 (D:\projects\test\go\struct\1\main.go:8) RET
0x0000 48 8d 05 00 00 00 00 48 89 44 24 08 c3 H......H.D$..
rel 3+4 t=16 runtime.zerobase+0
...

$0-8中0代表的是分配的栈帧大小为0,8代表的是各个实参和返回值的总大小

caller

LEAQ runtime.zerobase(SB), AX

runtime.zerobase(SB)的地址传送到AX寄存器

MOVQ AX, "".~r1+8(SP)

AX寄存器中的数据移动到~r1+8(SP)即返回值的位置

runtime.zerobase是个什么东西?

翻看源码找到runtime/malloc.go826行

1
2
// base address for all 0-byte allocations
var zerobase uintptr

是一个用于所有对内存零占用的结构的地址

所以空struct的地址就是zerobase,所以函数NewStructA才会返回相同的地址。

而且实际上前文所说的$0-8中的8代表的是各个实参和返回值的总大小,这个8也可以理解了,实参其实并没有占用栈空间,是返回值(空struct的指针)占用了栈空间的8个字节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"fmt"
"unsafe"
)

type A struct{}

func main() {
a := A{}
fmt.Println(unsafe.Sizeof(a)) // print 0
fmt.Println(unsafe.Sizeof(&a)) // print 8
}

空struct特性

  1. 以前一直不理解chan struct{}的意图,其实可以理解为struct{}是golang中最小的数据结构

    1
    2
    3
    4
    5
    6
    done := make(chan struct{})
    go func() {
    // ...
    done <- struct{}{}
    }()
    <- done
  2. map[string]struct{}map[string]bool更节约空间

  3. var x [1000000000]struct{}
    fmt.Println(unsafe.Sizeof(x)) // print 0
    

参考资料

附录

助记符 指令种类 用途 示例
MOVQ 传送 数据传送 MOVQ 48, AX // 把 48 传送到 AX
LEAQ 传送 地址传送 LEAQ AX, BX // 把 AX 有效地址传送到 BX
PUSHQ 传送 栈压入 PUSHQ AX // 将 AX 内容送入栈顶位置
POPQ 传送 栈弹出 POPQ AX // 弹出栈顶数据后修改栈顶指针
ADDQ 运算 相加并赋值 ADDQ BX, AX // 等价于 AX+=BX
SUBQ 运算 相减并赋值 SUBQ BX, AX // 等价于 AX-=BX
CMPQ 运算 比较大小 CMPQ SI CX // 比较 SI 和 CX 的大小
CALL 转移 调用函数 CALL runtime.printnl(SB) // 发起调用
JMP 转移 无条件转移指令 JMP 0x0185 //无条件转至 0x0185 地址处
JLS 转移 条件转移指令 JLS 0x0185 //左边小于右边,则跳到 0x0185

在mac下idea的terminal没办法使用option+左右方向键跳过单词,google后找到以下解决方案:

非zsh用户

Add this to ~/.zshrc:

1
2
"\e\eC": forward-word
"\e\eD": backward-word

zsh用户

Add this to ~/.zshrc:

1
2
bindkey "\e\eOD" backward-word
bindkey "\e\eOC" forward-word

前两天,logstash服务的日志一直在报错:

[FORBIDDEN/12/index read-only / allow delete (api)]

登上es所在的服务器看一眼,发现磁盘满了,挂载在/根目录的磁盘只有50G,虽然并不是es的存储导致的(因为已经设置了好了volums),但是docker容器没有空间也会导致服务不正常,所以打算把docker服务的目录都迁移至另一个分区。

准备工作

查看当前docker的Root Dir

1
2
# docker info | grep "Docker Root Dir"
Docker Root Dir: /var/lib/docker

这个目录是docker默认的安装目录,不论是构建镜像还是创建容器都是存储在该目录下的,如果磁盘满了,甚至会导致docker容器不能重启。

假设我们/home分区有足够的空间,我们会将/var/lib/docker迁移至/home/var/lib/docker,创建一下这个目录

mkdir -p /home/var/lib/docker/

开始迁移

停止docker服务

systemctl stop docker

利用rsync同步目录文件

rsync -avz /var/lib/docker/ /home/var/lib/docker

经过漫长的等待后,目录终于同步完成了

接下来编辑docker配置文件,没有则创建

例如:

vim /etc/docker/daemon.json

1
2
3
4
{
"data-root": "/home/var/lib/docker",
"registry-mirrors": ["mirror.docker.example.com"]
}

启动docker服务

systemctl start docker

再次执行docker info

1
2
# docker info | grep "Docker Root Dir"
Docker Root Dir: /home/var/lib/docker

再启动各个容器,查看服务是否正常。

这样就迁移好了。

前言

闲来无聊,做一些简单的单链表题

题目链接如下:

206. 反转链表

题目

反转一个单链表。

示例:

输入: 1->2->3->4->5->NULL

输出: 5->4->3->2->1->NULL

进阶:

你可以迭代或递归地反转链表。你能否用两种方法解决这道题?

思路

先定义一个新链表new_list,指针cur用于遍历

反转本质就是:

  1. cur的下一个节点连接到new_list,即cur.next=new_list

  2. 再将new_list赋值为cur

  3. 然后将cur赋值为原本cur.next,并回到第一步继续遍历

代码

1
2
3
4
5
6
7
8
9
10
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
new_list = None
cur = head
while cur != None:
tmp = cur.next
cur.next = new_list
new_list = cur
cur = tmp
return new_list

141. 环形链表

题目

给定一个链表,判断链表中是否有环。

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。

 

示例 1:

输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

输入:head = [1], pos = -1
输出:false
解释:链表中没有环。

 

进阶:

你能用 O(1)(即,常量)内存解决此问题吗?

思路

检测单链表中是否有环,经典问题了,定义步长不同的两个指针,慢指针每次走一步,快指针每次走两步,如果有环,它们一定会相遇。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution:
def hasCycle(self, head: ListNode) -> bool:
if head is None or head.next is None:
return False
slow = head
fast = head.next
while slow != fast:
if fast.next is None or fast.next.next is None:
return False
else:
fast = fast.next.next
slow = slow.next
return True

142. 环形链表 II

题目

给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。

说明:不允许修改给定的链表。

 

示例 1:

输入:head = [3,2,0,-4], pos = 1
输出:tail connects to node index 1
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

输入:head = [1,2], pos = 0
输出:tail connects to node index 0
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

输入:head = [1], pos = -1
输出:no cycle
解释:链表中没有环。

思路

先给结论,在上一题中,我们引入了快慢指针,在它们相遇的时候,慢指针并没有遍历完链表,所以再设置一个指针从链表头部开始遍历,这两个指针相遇的点,就是链表环的入口。

我们来证明一下:

假设进入环之前的长度为x

快慢指针相遇时,进入环之后走过的长度为a

环的长度为C

所以当它们相遇时,慢指针走了x+a步,快指针走了x+kC+a

其中,k为正整数,表示快指针走了多少圈,但实际上走多少圈都是没关系的,相对位置没有变,所以我们为了简单,取1即可。

而又因为慢指针每次走1步,快指针每次走2步

所以快指针走过的路程是慢指针的两倍,即2(x+a)

显然x+C+a = 2(x+a),即x = C-a

C-a又正好是慢指针还没有走完的路程

所以当快慢指针相遇时,再设置一个指针从链表头部开始遍历,每次也走1步。新指针和慢指针相遇的点,就是链表环的入口。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution:
def detectCycle(self, head: ListNode) -> ListNode:
if head is None or head.next is None:
return None
slow = head
fast = head
while fast is not None and fast.next is not None:
fast = fast.next.next
slow = slow.next
if fast == slow:
new_slow = head
while new_slow != slow:
new_slow = new_slow.next
slow = slow.next
return new_slow
return None

876. 链表的中间结点

题目

给定一个带有头结点 head 的非空单链表,返回链表的中间结点。

如果有两个中间结点,则返回第二个中间结点。

 

示例 1:

输入:[1,2,3,4,5]
输出:此列表中的结点 3 (序列化形式:[3,4,5])
返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。
注意,我们返回了一个 ListNode 类型的对象 ans,这样:
ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.
示例 2:

输入:[1,2,3,4,5,6]
输出:此列表中的结点 4 (序列化形式:[4,5,6])
由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点。  

提示:

给定链表的结点数介于 1 和 100 之间。

思路

很简单,略

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def middleNode(self, head: ListNode) -> ListNode:
if head is None or head.next is None:
return head
middle = head
cur = head
i = 1
j = 1
while cur is not None and cur.next is not None:
cur = cur.next
j += 1
if i*2 <= j:
i += 1
middle = middle.next
return middle

21. 合并两个有序链表

题目

将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 

示例:

输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4

思路

很简单,略

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution:
def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
cur = ListNode(0)
if l1 is None:
return l2
if l2 is None:
return l1
l3 = cur
while l1 is not None or l2 is not None:
if l1 is None and l2 is not None:
cur.next = l2
l2 = l2.next
elif l2 is None and l1 is not None:
cur.next = l1
l1 = l1.next
elif l1.val < l2.val:
cur.next = l1
l1 = l1.next
else:
cur.next = l2
l2 = l2.next
cur = cur.next
return l3.next