RegMs If 3 rokov pred
rodič
commit
0d40f5db91

+ 1 - 1
source/_posts/coding/digital-image-processing-assignment-i.md

@@ -1,7 +1,7 @@
 ---
 title: Digital Image Processing Assignment I
 tags:
-  - Assignment
+  - Course Project
 id: "3418"
 categories:
   - - Coding

+ 1 - 1
source/_posts/coding/digital-image-processing-assignment-ii.md

@@ -1,7 +1,7 @@
 ---
 title: Digital Image Processing Assignment II
 tags:
-  - Assignment
+  - Course Project
 id: "3453"
 categories:
   - - Coding

+ 1 - 1
source/_posts/coding/digital-image-processing-assignment-iii.md

@@ -1,7 +1,7 @@
 ---
 title: Digital Image Processing Assignment III
 tags:
-  - Assignment
+  - Course Project
 id: "3472"
 categories:
   - - Coding

+ 1 - 1
source/_posts/coding/digital-image-processing-assignment-iv.md

@@ -1,7 +1,7 @@
 ---
 title: Digital Image Processing Assignment IV
 tags:
-  - Assignment
+  - Course Project
 id: "3518"
 categories:
   - - Coding

+ 823 - 0
source/_posts/coding/go-语言函数调用汇编代码分析.md

@@ -0,0 +1,823 @@
+---
+title: Go 语言函数调用汇编代码分析
+tags:
+  - Course Project
+categories:
+  - - Coding
+    - Programming Language
+date: 2021-10-30 10:54:00
+---
+
+# 背景说明
+
+Go 语言是 Google 开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言,它用批判吸收的眼光,融合 C 语言、Java 等众家之长,将简洁、高效演绎得淋漓尽致。
+
+这篇文章主要研究 Go 语言中的函数调用是如何用 x64 汇编实现的。
+
+# 探索过程
+
+首先编写一个非常简单的 Go 语言程序。
+
+### _test1.go_
+
+```go
+package main
+
+func sum(a, b int) int {
+	return a + b
+}
+
+func main() {
+	sum(1, 2)
+}
+```
+
+程序中定义了 sum 函数,用于计算两个整型变量的和并返回。main 函数中调用了 sum 函数。期望通过这个程序,研究 **Go 语言中函数调用的实现原理**。
+
+使用 `go tool compile -S test1.go`,可以生成 `test1.o` 目标文件并在屏幕上输出汇编代码。
+
+```x86asm
+"".sum STEXT nosplit size=4 args=0x10 locals=0x0 funcid=0x0
+        0x0000 00000 (test1.go:3)       TEXT    "".sum(SB), NOSPLIT|ABIInternal, $0-16
+        0x0000 00000 (test1.go:3)       FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
+        0x0000 00000 (test1.go:3)       FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
+        0x0000 00000 (test1.go:3)       FUNCDATA        $5, "".sum.arginfo1(SB)
+        0x0000 00000 (test1.go:4)       ADDQ    BX, AX
+        0x0003 00003 (test1.go:4)       RET
+        0x0000 48 01 d8 c3                                      H...
+"".main STEXT nosplit size=1 args=0x0 locals=0x0 funcid=0x0
+        0x0000 00000 (test1.go:7)       TEXT    "".main(SB), NOSPLIT|ABIInternal, $0-0
+        0x0000 00000 (test1.go:7)       FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
+        0x0000 00000 (test1.go:7)       FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
+        0x0000 00000 (test1.go:9)       RET
+        0x0000 c3                                               .
+go.cuinfo.packagename. SDWARFCUINFO dupok size=0
+        0x0000 6d 61 69 6e                                      main
+""..inittask SNOPTRDATA size=24
+        0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
+        0x0010 00 00 00 00 00 00 00 00                          ........
+go.info."".sum$abstract SDWARFABSFCN dupok size=25
+        0x0000 04 2e 73 75 6d 00 01 01 11 61 00 00 00 00 00 00  ..sum....a......
+        0x0010 11 62 00 00 00 00 00 00 00                       .b.......
+        rel 0+0 t=23 type.int+0
+        rel 12+4 t=31 go.info.int+0
+        rel 20+4 t=31 go.info.int+0
+gclocals·33cdeccccebe80329f1fdbee7f5874cb SRODATA dupok size=8
+        0x0000 01 00 00 00 00 00 00 00                          ........
+"".sum.arginfo1 SRODATA static dupok size=5
+        0x0000 00 08 08 08 ff                                   .....
+```
+
+代码中出现了一些不属于 x64 汇编的指令。在 Go 语言的官方文档中可以查阅到一些 Go 语言专用的伪指令。`TEXT` 伪指令实际上定义了一个函数,比如 `TEXT "".sum(SB), NOSPLIT|ABIInternal, $0-16` 定义了一个叫 sum 的函数,`$0-16` 表示函数帧的大小为 0 字节,参数大小为 16 字节(即两个整型变量的大小)。`FUNCDATA` 伪指令为垃圾回收器提供信息。
+
+使用 `go tool objdump -S -gnu test1.o` 命令可以更方便地查看汇编代码,因为它去除了 Go 语言专属的伪指令,并且可以显示 GNU 汇编。
+
+```x86asm
+TEXT "".sum(SB) gofile../home/regmsif/test/goasm/test1.go
+        return a + b
+  0x561                 4801d8                  ADDQ BX, AX                          // add %rbx,%rax
+  0x564                 c3                      RET                                  // retq
+
+TEXT "".main(SB) gofile../home/regmsif/test/goasm/test1.go
+}
+  0x565                 c3                      RET                                  // retq
+```
+
+sum 函数直接将 rbx 寄存器的值加到 rax 上并返回。可以推测 Go 语言调用函数时通常会**将参数依次放在 rax、rbx 等寄存器,将返回值放在 rax 寄存器**。
+
+main 函数里其实只执行了 `retq` 指令,也就是说并没有调用 sum 函数,这与预期不符。猜测这是因为我们没有将 sum 函数的返回值赋给其他变量,所以 Go 语言自动将函数调用优化了。尝试将 sum 函数的返回值赋给一个变量。
+
+```go
+package main
+
+func sum(a, b int) int {
+	return a + b
+}
+
+func main() {
+	c := sum(1, 2)
+}
+```
+
+然而这份代码无法通过编译。编译错误提示 `test2.go:8:2: c declared but not used`。这是因为 Go 语言不允许定义不会被使用的变量。既然一定要使用,那就继续用这个变量调用一次 sum 函数。
+
+### _test2.go_
+
+```go
+package main
+
+func sum(a, b int) int {
+	return a + b
+}
+
+func main() {
+	c := sum(1, 2)
+	sum(c, 3)
+}
+```
+
+汇编代码如下。
+
+```x86asm
+TEXT "".sum(SB) gofile../home/regmsif/test/goasm/test2.go
+        return a + b
+  0x561                 4801d8                  ADDQ BX, AX                          // add %rbx,%rax
+  0x564                 c3                      RET                                  // retq
+
+TEXT "".main(SB) gofile../home/regmsif/test/goasm/test2.go
+}
+  0x565                 c3                      RET                                  // retq
+```
+
+发现仍然没有调用 sum 函数。猜测这是因为 Go 语言编译时会构建一棵函数调用的**依赖关系树**。如果一个函数不被任何其他操作依赖,则这个函数不会被调用。为了验证这一猜想,尝试编写一个具有较为复杂的嵌套关系的代码。
+
+### _test3.go_
+
+```go
+package main
+
+func sum(a, b int) int {
+	return a + b
+}
+
+func mul(a, b int) int {
+	return a * b
+}
+
+func main() {
+	if sum(1, 2) == 0 {
+		if mul(3, 4) == 0 {
+			sum(5, 6)
+		}
+		if sum(7, 8) == 0 && mul(9, 10) == 0 {
+			mul(11, 12)
+		}
+	} else {
+		if mul(13, 14) == 0 || sum(15, 16) == 0 {
+			if sum(17, 18) == 0 {
+				mul(19, 20)
+			}
+		}
+	}
+}
+```
+
+汇编代码如下。
+
+```x86asm
+TEXT "".sum(SB) gofile../home/regmsif/test/goasm/test3.go
+        return a + b
+  0x795                 4801d8                  ADDQ BX, AX                          // add %rbx,%rax
+  0x798                 c3                      RET                                  // retq
+
+TEXT "".mul(SB) gofile../home/regmsif/test/goasm/test3.go
+        return a * b
+  0x799                 480fafc3                IMULQ BX, AX                         // imul %rbx,%rax
+  0x79d                 c3                      RET                                  // retq
+
+TEXT "".main(SB) gofile../home/regmsif/test/goasm/test3.go
+}
+  0x79e                 c3                      RET                                  // retq
+```
+
+可以发现,即使嵌套了很多层,编译器依然判定这些代码都不需要。那么,究竟什么样的函数才是必须执行的呢?被忽略的函数都是给定一些参数,返回一个值,不会对外界产生影响,也就是说,它们没有**副作用**。从另一方面想,正是因为这些函数没有副作用,所以在它们的返回值被忽略时,它们也可以被忽略。而有副作用的函数会对外界产生影响,如果忽略它们,可能影响程序的结果。
+
+尝试为 sum 函数添加副作用。
+
+### _test4.go_
+
+```go
+package main
+
+var c int
+
+func sum(a, b int) int {
+	c = a + b
+	return c
+}
+
+func main() {
+	sum(1, 2)
+	sum(c, 3)
+}
+```
+
+汇编代码如下。
+
+```x86asm
+TEXT "".sum(SB) gofile../home/regmsif/test/goasm/test4.go
+        c = a + b
+  0x5d4                 4801d8                  ADDQ BX, AX                          // add %rbx,%rax
+  0x5d7                 48890500000000          MOVQ AX, 0(IP)                       // mov %rax,(%rip) [3:7]R_PCREL:"".c
+        return c
+  0x5de                 c3                      RET                                  // retq
+
+TEXT "".main(SB) gofile../home/regmsif/test/goasm/test4.go
+        sum(1, 2)
+  0x5df                 48c7050000000003000000  MOVQ $0x3, 0(IP)                     // movq $0x3,(%rip)        [3:7]R_PCREL:"".c+-4
+        sum(c, 3)
+  0x5ea                 90                      NOPL                                 // nop
+        c = a + b
+  0x5eb                 48c7050000000006000000  MOVQ $0x6, 0(IP)                     // movq $0x6,(%rip)        [3:7]R_PCREL:"".c+-4
+}
+  0x5f6                 c3                      RET                                  // retq
+```
+
+main 函数里出现了新的指令 `movq $0x3,(%rip)`,它表示将 3 放到一个**相对于 rip 的地址**(即变量 c 的地址)上。这个相对地址在代码中为 0,但这并不意味着会把数据放在下一条指令的位置,因为编译器在链接之前还不知道这个数据会被安排在哪,所以 0 相当于一个占位符,地址的具体值将在链接时填充。
+
+通过 `go build test4.go` 生成可执行文件,再查看汇编代码中的 main 函数。
+
+```x86asm
+TEXT main.main(SB) /home/regmsif/test/goasm/test4.go
+        sum(1, 2)
+  0x4553e0              48c705754b090003000000  MOVQ $0x3, main.c(SB)                // movq $0x3,0x94b75(%rip)
+        sum(c, 3)
+  0x4553eb              90                      NOPL                                 // nop
+        c = a + b
+  0x4553ec              48c705694b090006000000  MOVQ $0x6, main.c(SB)                // movq $0x6,0x94b69(%rip)
+}
+  0x4553f7              c3                      RET                                  // retq
+```
+
+可以发现指令变为 `movq $0x3,0x94b75(%rip)`。然而,在生成的可执行文件中并没有发现 sum 函数。这可能是因为编译器自动计算了函数的返回值并作为常量(3 和 6)写在汇编代码中。还有个问题是不清楚为什么要在第一条 `movq` 指令和第二条 `movq` 指令之间插入一条 `nop` 指令。猜测是为了解决 CPU 流水线竞争。
+
+尝试将 sum 函数放在循环中,观察是否会自动计算。
+
+### _test5.go_
+
+```go
+package main
+
+var c int
+
+func sum(a, b int) int {
+	c = a + b
+	return c
+}
+
+func main() {
+	for i := 1; i <= 100; i++ {
+		sum(i, i+1)
+	}
+}
+```
+
+汇编代码如下。
+
+```x86asm
+TEXT "".sum(SB) gofile../home/regmsif/test/goasm/test5.go
+        c = a + b
+  0x5bd                 4801d8                  ADDQ BX, AX                          // add %rbx,%rax
+  0x5c0                 48890500000000          MOVQ AX, 0(IP)                       // mov %rax,(%rip) [3:7]R_PCREL:"".c
+        return c
+  0x5c7                 c3                      RET                                  // retq
+
+TEXT "".main(SB) gofile../home/regmsif/test/goasm/test5.go
+func main() {
+  0x5c8                 b801000000              MOVL $0x1, AX                        // mov $0x1,%eax
+        for i := 1; i <= 100; i++ {
+  0x5cd                 eb19                    JMP 0x5e8                            // jmp 0x5e8
+                sum(i, i+1)
+  0x5cf                 90                      NOPL                                 // nop
+        c = a + b
+  0x5d0                 488d0c00                LEAQ 0(AX)(AX*1), CX                 // lea (%rax,%rax,1),%rcx
+  0x5d4                 488d4901                LEAQ 0x1(CX), CX                     // lea 0x1(%rcx),%rcx
+  0x5d8                 48890d00000000          MOVQ CX, 0(IP)                       // mov %rcx,(%rip)         [3:7]R_PCREL:"".c
+        for i := 1; i <= 100; i++ {
+  0x5df                 48ffc0                  INCQ AX                              // inc %rax
+  0x5e2                 660f1f440000            NOPW 0(AX)(AX*1)                     // nopw (%rax,%rax,1)
+  0x5e8                 4883f864                CMPQ $0x64, AX                       // cmp $0x64,%rax
+  0x5ec                 7ee1                    JLE 0x5cf                            // jle 0x5cf
+}
+  0x5ee                 c3                      RET                                  // retq
+```
+
+编译器并没有自动计算最终结果。不过,它也没有调用 sum 函数。实际上,它直接使用了 `lea` 指令来计算两个数之和。在这个例子里需要计算 `i + (i + 1)`,第一条 `lea` 指令 `lea (%rax,%rax,1),%rcx` 计算了 `i + 1 * i`,第二条 `lea` 指令 `lea 0x1(%rcx),%rcx` 把前一条指令的结果加 1。
+
+尝试将 sum 函数里的 `c = a + b` 改成其他的式子(比如 `c = a/5 + b*123 + 4`),汇编代码如下。
+
+### _test6.go_
+
+```x86asm
+TEXT "".sum(SB) gofile../home/regmsif/test/goasm/test6.go
+        c = a/5 + b*123 + 4
+  0x5bd                 4889c1                  MOVQ AX, CX                          // mov %rax,%rcx
+  0x5c0                 48b8cdcccccccccccccc    MOVQ $0xcccccccccccccccd, AX         // mov $-0x3333333333333333,%rax
+  0x5ca                 48f7e9                  IMULQ CX                             // imul %rcx
+  0x5cd                 4801ca                  ADDQ CX, DX                          // add %rcx,%rdx
+  0x5d0                 48c1fa02                SARQ $0x2, DX                        // sar $0x2,%rdx
+  0x5d4                 48c1f93f                SARQ $0x3f, CX                       // sar $0x3f,%rcx
+  0x5d8                 4829ca                  SUBQ CX, DX                          // sub %rcx,%rdx
+  0x5db                 486bcb7b                IMULQ $0x7b, BX, CX                  // imul $0x7b,%rbx,%rcx
+  0x5df                 488d0411                LEAQ 0(CX)(DX*1), AX                 // lea (%rcx,%rdx,1),%rax
+  0x5e3                 488d4004                LEAQ 0x4(AX), AX                     // lea 0x4(%rax),%rax
+  0x5e7                 48890500000000          MOVQ AX, 0(IP)                       // mov %rax,(%rip)                 [3:7]R_PCREL:"".c
+        return c
+  0x5ee                 c3                      RET                                  // retq
+
+TEXT "".main(SB) gofile../home/regmsif/test/goasm/test6.go
+func main() {
+  0x5ef                 b801000000              MOVL $0x1, AX                        // mov $0x1,%eax
+        for i := 1; i <= 100; i++ {
+  0x5f4                 eb2f                    JMP 0x625                            // jmp 0x625
+                sum(i, i+1)
+  0x5f6                 90                      NOPL                                 // nop
+        for i := 1; i <= 100; i++ {
+  0x5f7                 4889c1                  MOVQ AX, CX                          // mov %rax,%rcx
+        c = a/5 + b*123 + 4
+  0x5fa                 48b8cdcccccccccccccc    MOVQ $0xcccccccccccccccd, AX         // mov $-0x3333333333333333,%rax
+  0x604                 48f7e9                  IMULQ CX                             // imul %rcx
+  0x607                 4801ca                  ADDQ CX, DX                          // add %rcx,%rdx
+  0x60a                 48c1fa02                SARQ $0x2, DX                        // sar $0x2,%rdx
+  0x60e                 486bd97b                IMULQ $0x7b, CX, BX                  // imul $0x7b,%rcx,%rbx
+  0x612                 488d141a                LEAQ 0(DX)(BX*1), DX                 // lea (%rdx,%rbx,1),%rdx
+  0x616                 488d527f                LEAQ 0x7f(DX), DX                    // lea 0x7f(%rdx),%rdx
+  0x61a                 48891500000000          MOVQ DX, 0(IP)                       // mov %rdx,(%rip)                 [3:7]R_PCREL:"".c
+        for i := 1; i <= 100; i++ {
+  0x621                 488d4101                LEAQ 0x1(CX), AX                     // lea 0x1(%rcx),%rax
+  0x625                 4883f864                CMPQ $0x64, AX                       // cmp $0x64,%rax
+  0x629                 7ecb                    JLE 0x5f6                            // jle 0x5f6
+}
+  0x62b                 c3                      RET                                  // retq
+```
+
+可以发现不管是在 sum 函数中还是在 main 函数中,编译器都对这个式子的计算做了优化,但仔细观察可知这是两个不同的式子:sum 函数里最后一步计算为 `lea 0x4(%rax),%rax`,对应的式子就是 `a/5 + b*123 + 4`;而 main 函数里最后一步计算为 `lea 0x7f(%rdx),%rdx`,对应的式子其实是 `i/5 + i*123 + 127 = i/5 + (i+1)*123 + 4`。也就是说,编译器似乎自动理解了 sum 函数要做的工作,并把工作的内容嵌入了调用 sum 函数的地方。
+
+以上这些都与传统意义上对函数调用的认知不同。一般来说,函数调用的步骤是先设置参数,接着跳转到函数的起始地址,最后通过 `retq` 指令返回,并取返回值。然而,这几个 Go 语言的程序都没有真正“调用” sum 函数。它们要么是无视了 sum 函数,要么是将 sum 函数以某种方式嵌入了调用它的地方。
+
+那么有什么办法可以让 Go 语言的程序“真正”调用函数呢?其实,Go 语言所做的事情就是一类**编译优化**。可以通过 `go tool compile -m test1.go` 命令查看编译 _test1.go_ 时使用的优化选项。
+
+```x86asm
+test1.go:3:6: can inline sum
+test1.go:7:6: can inline main
+test1.go:8:5: inlining call to sum
+```
+
+显然,编译器使用了**内联函数**。调用函数时,编译器直接将函数内联嵌入调用的地方,而不通过 `callq` 指令跳转。
+
+为了关闭内联函数,在编译时添加 `-l` 选项。用 `go tool compile -l -m test1.go` 编译得到的汇编代码如下。
+
+```x86asm
+TEXT "".sum(SB) gofile../home/regmsif/test/goasm/test1.go
+        return a + b
+  0x551                 4801d8                  ADDQ BX, AX                          // add %rbx,%rax
+  0x554                 c3                      RET                                  // retq
+
+TEXT "".main(SB) gofile../home/regmsif/test/goasm/test1.go
+func main() {
+  0x555                 493b6610                CMPQ 0x10(R14), SP                   // cmp 0x10(%r14),%rsp
+  0x559                 7629                    JBE 0x584                            // jbe 0x584
+  0x55b                 4883ec18                SUBQ $0x18, SP                       // sub $0x18,%rsp
+  0x55f                 48896c2410              MOVQ BP, 0x10(SP)                    // mov %rbp,0x10(%rsp)
+  0x564                 488d6c2410              LEAQ 0x10(SP), BP                    // lea 0x10(%rsp),%rbp
+        sum(1, 2)
+  0x569                 b801000000              MOVL $0x1, AX                        // mov $0x1,%eax
+  0x56e                 bb02000000              MOVL $0x2, BX                        // mov $0x2,%ebx
+  0x573                 6690                    NOPW                                 // data16 nop
+  0x575                 e800000000              CALL 0x57a                           // callq 0x57a     [1:5]R_CALL:"".sum
+}
+  0x57a                 488b6c2410              MOVQ 0x10(SP), BP                    // mov 0x10(%rsp),%rbp
+  0x57f                 4883c418                ADDQ $0x18, SP                       // add $0x18,%rsp
+  0x583                 c3                      RET                                  // retq
+func main() {
+  0x584                 e800000000              CALL 0x589                           // callq 0x589     [1:5]R_CALL:runtime.morestack_noctxt
+  0x589                 ebca                    JMP "".main(SB)                      // jmp 0x555
+```
+
+这个汇编对我们来说就很熟悉了。调用函数之前,用 `mov $0x1,%eax` 和 `mov $0x2,%ebx` 指令设置参数,用 `callq 0x57a` 指令调用 sum 函数,用 `add %rbx,%rax` 指令计算结果并保存在 rax 寄存器,最后用 `retq` 指令返回。
+
+这意味着之前有关副作用的猜想并不准确。在这个程序中,sum 函数没有副作用,但它仍会被调用。事实上,如果在编译 _test3.go_ 时关闭内联函数,将发现它们每次都会被调用。
+
+Go 的编译器还有一个 `-N` 选项,可以关闭编译 _test1.go_ 时的优化。用 `go tool compile -N -m test1.go` 编译得到的汇编代码如下。
+
+```x86asm
+TEXT "".sum(SB) gofile../home/regmsif/test/goasm/test1.go
+func sum(a, b int) int {
+  0x561                 4883ec10                SUBQ $0x10, SP                       // sub $0x10,%rsp
+  0x565                 48896c2408              MOVQ BP, 0x8(SP)                     // mov %rbp,0x8(%rsp)
+  0x56a                 488d6c2408              LEAQ 0x8(SP), BP                     // lea 0x8(%rsp),%rbp
+  0x56f                 4889442418              MOVQ AX, 0x18(SP)                    // mov %rax,0x18(%rsp)
+  0x574                 48895c2420              MOVQ BX, 0x20(SP)                    // mov %rbx,0x20(%rsp)
+  0x579                 48c7042400000000        MOVQ $0x0, 0(SP)                     // movq $0x0,(%rsp)
+        return a + b
+  0x581                 488b442418              MOVQ 0x18(SP), AX                    // mov 0x18(%rsp),%rax
+  0x586                 4803442420              ADDQ 0x20(SP), AX                    // add 0x20(%rsp),%rax
+  0x58b                 48890424                MOVQ AX, 0(SP)                       // mov %rax,(%rsp)
+  0x58f                 488b6c2408              MOVQ 0x8(SP), BP                     // mov 0x8(%rsp),%rbp
+  0x594                 4883c410                ADDQ $0x10, SP                       // add $0x10,%rsp
+  0x598                 c3                      RET                                  // retq
+
+TEXT "".main(SB) gofile../home/regmsif/test/goasm/test1.go
+func main() {
+  0x599                 4883ec20                SUBQ $0x20, SP                       // sub $0x20,%rsp
+  0x59d                 48896c2418              MOVQ BP, 0x18(SP)                    // mov %rbp,0x18(%rsp)
+  0x5a2                 488d6c2418              LEAQ 0x18(SP), BP                    // lea 0x18(%rsp),%rbp
+        sum(1, 2)
+  0x5a7                 48c744241001000000      MOVQ $0x1, 0x10(SP)                  // movq $0x1,0x10(%rsp)
+  0x5b0                 48c744240802000000      MOVQ $0x2, 0x8(SP)                   // movq $0x2,0x8(%rsp)
+        return a + b
+  0x5b9                 488b442410              MOVQ 0x10(SP), AX                    // mov 0x10(%rsp),%rax
+  0x5be                 4883c002                ADDQ $0x2, AX                        // add $0x2,%rax
+        sum(1, 2)
+  0x5c2                 48890424                MOVQ AX, 0(SP)                       // mov %rax,(%rsp)
+  0x5c6                 eb00                    JMP 0x5c8                            // jmp 0x5c8
+}
+  0x5c8                 488b6c2418              MOVQ 0x18(SP), BP                    // mov 0x18(%rsp),%rbp
+  0x5cd                 4883c420                ADDQ $0x20, SP                       // add $0x20,%rsp
+  0x5d1                 c3                      RET                                  // retq
+```
+
+可以发现在函数内部多了很多代码(原来只有一条 `add %rbx,%rax`)。这些代码是用于创建函数栈帧、保存寄存器值的。在这个程序里,main 函数也没有直接调用 sum 函数,而是将 sum 函数的代码嵌入调用的地方(将 `%rax` 和 `%rbx` 替换成了 `$0x1` 和 `$0x2`)。这意味着编译器是**将编译优化与内联函数分开**的。它们可以同时打开,同时关闭,也可以只打开其中一个。对于 _test1.go_,打开或关闭这两个选项的效果可以总结为如下表格。
+
+|              |                                   关闭编译优化                                    |                                     打开编译优化                                      |
+| :----------: | :-------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------: |
+| 关闭内联函数 | 函数内部创建栈帧并保存寄存器<br />调用函数时,用 rax 和 rbx 传参,用 rax 取返回值 | 函数内部直接执行 `add %rbx,%rax`<br />调用函数时,用 rax 和 rbx 传参,用 rax 取返回值 |
+| 打开内联函数 |        函数内部创建栈帧并保存寄存器<br />调用函数时,将函数嵌入调用的地方         |                  函数内部直接执行 `add %rbx,%rax`<br /> 没有调用函数                  |
+
+到这里,为什么同时打开编译优化和内联函数时不会调用 sum 函数的问题就很明朗了。打开编译优化意味着 sum 函数不需要对栈进行操作,直接执行 `add %rbx,%rax`;打开内联函数意味着这条语句会被直接嵌入调用 sum 函数的地方,并且 `%rax` 和 `%rbx` 会被替换成 `$0x1` 和 `$0x2`,即 `add $0x1,$0x2`。但这并不是一条合法的汇编指令,因为 `add` 指令的第二个参数必须为寄存器或内存。于是,这条指令并不会产生任何效果,即被忽略了。
+
+这是不是意味着 sum 函数就没有用呢?尝试将 sum 函数作为 if 语句的条件,观察是否会生效。
+
+### _test7.go_
+
+```go
+package main
+
+var c int
+
+func sum(a, b int) int {
+	return a + b
+}
+
+func main() {
+	if sum(1, 2) == 3 {
+		c = 4
+	}
+}
+```
+
+汇编代码如下(打开编译优化和内联函数)。
+
+```x86asm
+TEXT "".sum(SB) gofile../home/regmsif/test/goasm/test7.go
+        return a + b
+  0x5a6                 4801d8                  ADDQ BX, AX                          // add %rbx,%rax
+  0x5a9                 c3                      RET                                  // retq
+
+TEXT "".main(SB) gofile../home/regmsif/test/goasm/test7.go
+                c = 4
+  0x5aa                 48c7050000000004000000  MOVQ $0x4, 0(IP)                     // movq $0x4,(%rip)        [3:7]R_PCREL:"".c+-4
+}
+  0x5b5                 c3                      RET                                  // retq
+```
+
+main 函数里只有 `movq $0x4,(%rip)` 和 `retq`,也就是说 `c = 4` 被直接执行了,并没有先做判断。这可能是因为编译器将 `add $0x1,$0x2` 看成常数 3,将它与 3 的比较结果看成布尔常数 true,所以忽略了判断直接执行内部语句。
+
+为了验证这一猜想,尝试将条件改为 `true`,汇编代码不变,符合预期。如果将条件改为 `sum(1, 2) != 3`,则 main 函数里只有 `retq`,因为比较结果为布尔常数 false,符合预期。如果关闭内联函数(或编译优化),则会先调用(或嵌入)函数进行判断,符合预期。
+
+尝试在一个函数内部调用另一个函数。
+
+### _test8.go_
+
+```go
+package main
+
+func mul(a, b int) int {
+	return a * b
+}
+
+func sum(a, b int) int {
+	return a + mul(a, b)
+}
+
+func main() {
+	sum(1, 2)
+}
+```
+
+汇编代码如下(关闭编译优化,打开内联函数)。
+
+```x86asm
+TEXT "".mul(SB) gofile../home/regmsif/test/goasm/test8.go
+func mul(a, b int) int {
+  0x795                 4883ec10                SUBQ $0x10, SP                       // sub $0x10,%rsp
+  0x799                 48896c2408              MOVQ BP, 0x8(SP)                     // mov %rbp,0x8(%rsp)
+  0x79e                 488d6c2408              LEAQ 0x8(SP), BP                     // lea 0x8(%rsp),%rbp
+  0x7a3                 4889442418              MOVQ AX, 0x18(SP)                    // mov %rax,0x18(%rsp)
+  0x7a8                 48895c2420              MOVQ BX, 0x20(SP)                    // mov %rbx,0x20(%rsp)
+  0x7ad                 48c7042400000000        MOVQ $0x0, 0(SP)                     // movq $0x0,(%rsp)
+        return a * b
+  0x7b5                 488b442420              MOVQ 0x20(SP), AX                    // mov 0x20(%rsp),%rax
+  0x7ba                 488b4c2418              MOVQ 0x18(SP), CX                    // mov 0x18(%rsp),%rcx
+  0x7bf                 480fafc1                IMULQ CX, AX                         // imul %rcx,%rax
+  0x7c3                 48890424                MOVQ AX, 0(SP)                       // mov %rax,(%rsp)
+  0x7c7                 488b6c2408              MOVQ 0x8(SP), BP                     // mov 0x8(%rsp),%rbp
+  0x7cc                 4883c410                ADDQ $0x10, SP                       // add $0x10,%rsp
+  0x7d0                 c3                      RET                                  // retq
+
+TEXT "".sum(SB) gofile../home/regmsif/test/goasm/test8.go
+func sum(a, b int) int {
+  0x7d1                 4883ec28                SUBQ $0x28, SP                       // sub $0x28,%rsp
+  0x7d5                 48896c2420              MOVQ BP, 0x20(SP)                    // mov %rbp,0x20(%rsp)
+  0x7da                 488d6c2420              LEAQ 0x20(SP), BP                    // lea 0x20(%rsp),%rbp
+  0x7df                 4889442430              MOVQ AX, 0x30(SP)                    // mov %rax,0x30(%rsp)
+  0x7e4                 48895c2438              MOVQ BX, 0x38(SP)                    // mov %rbx,0x38(%rsp)
+  0x7e9                 48c7042400000000        MOVQ $0x0, 0(SP)                     // movq $0x0,(%rsp)
+        return a + mul(a, b)
+  0x7f1                 488b4c2430              MOVQ 0x30(SP), CX                    // mov 0x30(%rsp),%rcx
+  0x7f6                 48894c2418              MOVQ CX, 0x18(SP)                    // mov %rcx,0x18(%rsp)
+  0x7fb                 488b4c2438              MOVQ 0x38(SP), CX                    // mov 0x38(%rsp),%rcx
+  0x800                 48894c2410              MOVQ CX, 0x10(SP)                    // mov %rcx,0x10(%rsp)
+        return a * b
+  0x805                 488b542418              MOVQ 0x18(SP), DX                    // mov 0x18(%rsp),%rdx
+  0x80a                 480fafd1                IMULQ CX, DX                         // imul %rcx,%rdx
+        return a + mul(a, b)
+  0x80e                 4889542408              MOVQ DX, 0x8(SP)                     // mov %rdx,0x8(%rsp)
+  0x813                 eb00                    JMP 0x815                            // jmp 0x815
+  0x815                 488b4c2430              MOVQ 0x30(SP), CX                    // mov 0x30(%rsp),%rcx
+  0x81a                 488d0411                LEAQ 0(CX)(DX*1), AX                 // lea (%rcx,%rdx,1),%rax
+  0x81e                 48890424                MOVQ AX, 0(SP)                       // mov %rax,(%rsp)
+  0x822                 488b6c2420              MOVQ 0x20(SP), BP                    // mov 0x20(%rsp),%rbp
+  0x827                 4883c428                ADDQ $0x28, SP                       // add $0x28,%rsp
+  0x82b                 c3                      RET                                  // retq
+
+TEXT "".main(SB) gofile../home/regmsif/test/goasm/test8.go
+func main() {
+  0x82c                 4883ec38                SUBQ $0x38, SP                       // sub $0x38,%rsp
+  0x830                 48896c2430              MOVQ BP, 0x30(SP)                    // mov %rbp,0x30(%rsp)
+  0x835                 488d6c2430              LEAQ 0x30(SP), BP                    // lea 0x30(%rsp),%rbp
+        sum(1, 2)
+  0x83a                 48c744242801000000      MOVQ $0x1, 0x28(SP)                  // movq $0x1,0x28(%rsp)
+  0x843                 48c744241802000000      MOVQ $0x2, 0x18(SP)                  // movq $0x2,0x18(%rsp)
+        return a + mul(a, b)
+  0x84c                 488b442428              MOVQ 0x28(SP), AX                    // mov 0x28(%rsp),%rax
+  0x851                 4889442420              MOVQ AX, 0x20(SP)                    // mov %rax,0x20(%rsp)
+  0x856                 488b442418              MOVQ 0x18(SP), AX                    // mov 0x18(%rsp),%rax
+  0x85b                 4889442410              MOVQ AX, 0x10(SP)                    // mov %rax,0x10(%rsp)
+        return a * b
+  0x860                 488b4c2420              MOVQ 0x20(SP), CX                    // mov 0x20(%rsp),%rcx
+  0x865                 480fafc8                IMULQ AX, CX                         // imul %rax,%rcx
+        return a + mul(a, b)
+  0x869                 48890c24                MOVQ CX, 0(SP)                       // mov %rcx,(%rsp)
+  0x86d                 eb00                    JMP 0x86f                            // jmp 0x86f
+  0x86f                 488b442428              MOVQ 0x28(SP), AX                    // mov 0x28(%rsp),%rax
+  0x874                 4801c8                  ADDQ CX, AX                          // add %rcx,%rax
+        sum(1, 2)
+  0x877                 4889442408              MOVQ AX, 0x8(SP)                     // mov %rax,0x8(%rsp)
+  0x87c                 eb00                    JMP 0x87e                            // jmp 0x87e
+}
+  0x87e                 488b6c2430              MOVQ 0x30(SP), BP                    // mov 0x30(%rsp),%rbp
+  0x883                 4883c438                ADDQ $0x38, SP                       // add $0x38,%rsp
+  0x887                 c3                      RET                                  // retq
+```
+
+其效果相当于先将 mul 函数嵌入 sum 函数中调用的地方,接着将 sum 函数嵌入 main 函数中调用的地方。
+
+尝试在一个函数内部调用自身。
+
+### _test9.go_
+
+```go
+package main
+
+func sum(a, b int) int {
+	return sum(a, b)
+}
+
+func main() {
+	sum(1, 2)
+}
+```
+
+汇编代码如下(打开编译优化和内联函数)。
+
+```x86asm
+TEXT "".sum(SB) gofile../home/regmsif/test/goasm/test9.go
+func sum(a, b int) int {
+  0x5fb                 493b6610                CMPQ 0x10(R14), SP                   // cmp 0x10(%r14),%rsp
+  0x5ff                 761d                    JBE 0x61e                            // jbe 0x61e
+  0x601                 4883ec18                SUBQ $0x18, SP                       // sub $0x18,%rsp
+  0x605                 48896c2410              MOVQ BP, 0x10(SP)                    // mov %rbp,0x10(%rsp)
+  0x60a                 488d6c2410              LEAQ 0x10(SP), BP                    // lea 0x10(%rsp),%rbp
+        return sum(a, b)
+  0x60f                 e800000000              CALL 0x614                           // callq 0x614             [1:5]R_CALL:"".sum
+  0x614                 488b6c2410              MOVQ 0x10(SP), BP                    // mov 0x10(%rsp),%rbp
+  0x619                 4883c418                ADDQ $0x18, SP                       // add $0x18,%rsp
+  0x61d                 c3                      RET                                  // retq
+func sum(a, b int) int {
+  0x61e                 4889442408              MOVQ AX, 0x8(SP)                     // mov %rax,0x8(%rsp)
+  0x623                 48895c2410              MOVQ BX, 0x10(SP)                    // mov %rbx,0x10(%rsp)
+  0x628                 e800000000              CALL 0x62d                           // callq 0x62d             [1:5]R_CALL:runtime.morestack_noctxt
+  0x62d                 488b442408              MOVQ 0x8(SP), AX                     // mov 0x8(%rsp),%rax
+  0x632                 488b5c2410              MOVQ 0x10(SP), BX                    // mov 0x10(%rsp),%rbx
+  0x637                 ebc2                    JMP "".sum(SB)                       // jmp 0x5fb
+
+TEXT "".main(SB) gofile../home/regmsif/test/goasm/test9.go
+func main() {
+  0x639                 493b6610                CMPQ 0x10(R14), SP                   // cmp 0x10(%r14),%rsp
+  0x63d                 7629                    JBE 0x668                            // jbe 0x668
+  0x63f                 4883ec18                SUBQ $0x18, SP                       // sub $0x18,%rsp
+  0x643                 48896c2410              MOVQ BP, 0x10(SP)                    // mov %rbp,0x10(%rsp)
+  0x648                 488d6c2410              LEAQ 0x10(SP), BP                    // lea 0x10(%rsp),%rbp
+        sum(1, 2)
+  0x64d                 b801000000              MOVL $0x1, AX                        // mov $0x1,%eax
+  0x652                 bb02000000              MOVL $0x2, BX                        // mov $0x2,%ebx
+  0x657                 6690                    NOPW                                 // data16 nop
+  0x659                 e800000000              CALL 0x65e                           // callq 0x65e     [1:5]R_CALL:"".sum
+}
+  0x65e                 488b6c2410              MOVQ 0x10(SP), BP                    // mov 0x10(%rsp),%rbp
+  0x663                 4883c418                ADDQ $0x18, SP                       // add $0x18,%rsp
+  0x667                 c3                      RET                                  // retq
+func main() {
+  0x668                 e800000000              CALL 0x66d                           // callq 0x66d     [1:5]R_CALL:runtime.morestack_noctxt
+  0x66d                 ebca                    JMP "".main(SB)                      // jmp 0x639
+```
+
+虽然打开了内联函数,但编译器认为 sum 函数不可内联,因此使用传统的函数调用方法。这是因为内联函数的代码必须是确定的,而递归调用的深度是不确定的。
+
+通过以上这些实验,基本了解了 Go 语言对于函数调用的实现。总结一下就是:
+
+1. Go 语言编译器默认会打开编译优化和内联函数;
+2. 编译优化将尽可能减少函数调用前后的栈操作,减少内存访问,从而加快速度;
+3. 内联函数则直接将函数嵌入调用的地方,直接避免了函数调用,因此也减少了栈操作和内存访问,从而加快速度;
+4. 虽然它们目的一样,但编译优化在加快速度的同时可以减少可执行文件的大小(函数内部代码减少了),而内联函数则有可能增加可执行文件的大小(每个函数调用都会被替换成函数代码);
+5. 另外,在同时打开编译优化和内联函数时,编译器会自动将一些可以确定结果为常数的函数调用替换成其结果。
+
+当然,编译器也不会将所有可以内联的函数都进行内联,因为如果函数代码较长且需要在多个位置调用,打开内联函数的可执行文件大小将远远大于关闭内联函数的可执行文件。
+
+# 效果分析
+
+在这一部分,将对比同一份代码在不同编译选项下的目标文件大小以及运行速度。
+
+### 无副作用函数
+
+首先比较**目标文件大小**。由于编译器不会内联过长的函数,因此需要找一个尽可能长但又会被内联的函数,使对比更加明显。
+
+经过测试后发现,如下函数会被内联。
+
+```go
+func sum(a, b int) int {
+	c := a + b*1
+	d := b - c/2
+	e := c*d + 3
+	f := d/e - 4
+	g := e + f*5
+	h := f - g/6
+	i := g*h + 7
+	j := h/i - 8
+	return j
+}
+```
+
+而如下函数不会被内联。
+
+```go
+func sum(a, b int) int {
+	c := a + b*1
+	d := b - c/2
+	e := c*d + 3
+	f := d/e - 4
+	g := e + f*5
+	h := f - g/6
+	i := g*h + 7
+	j := h/i - 8
+	k := i + j*9 // add a line here
+	return k
+}
+```
+
+另外,编译器也不会内联调用的位置过多的函数。测试后发现 1240 个不同位置的 sum 函数调用会被内联,1250 个不同位置的 sum 函数调用不会被内联。
+
+因此,测试程序 **_test10.go_** 为在 1240 个不同的位置调用第一个 sum 函数。生成的目标文件大小(单位:B)如下。
+
+|              |    关闭编译优化     |    打开编译优化    |
+| :----------: | :-----------------: | :----------------: |
+| 关闭内联函数 | 55988 (test10.0.o)  | 55914 (test10.2.o) |
+| 打开内联函数 | 596280 (test10.1.o) | 2778 (test10.3.o)  |
+
+对于 test10.3.o,编译器自动忽略了所有函数调用,main 函数里只有 `retq`,所以大小最小。
+
+对于 test10.2.o,每个函数调用需要两条 `mov` 指令和一条 `callq` 指令(有些还需要一条 `nop` 指令),因此相比 test10.3.o 要大很多。
+
+对于 test10.0.o,函数调用部分与 test.10.2.o 相同,但由于关闭了编译优化,函数内部的代码更多了,因此相比 test10.2.o 稍大一点。
+
+对于 test10.1.o,每个函数调用都被替换成函数代码,所以相比其他目标文件又大了很多。
+
+以上结果符合预期。
+
+为了比较**运行速度**,需要足够多的调用次数。这可以用循环实现。因此,测试程序 **_test11.go_** 为循环调用第一个 sum 函数 100000000 次。编写如下 C 语言程序,用于运行 100 次测试程序并取平均运行时间。
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/time.h>
+
+int main()
+{
+    char cmd[100];
+    fgets(cmd, 100, stdin); // get command
+
+    struct timeval start, end;
+    long long total_time = 0, cnt = 100; // run 100 times
+    for (int i = 1; i <= cnt; i++)
+    {
+        gettimeofday(&start, NULL);
+        system(cmd); // run command
+        gettimeofday(&end, NULL);
+        total_time += (end.tv_sec - start.tv_sec) * 1000000 + (end.tv_usec - start.tv_usec); // calculate total time
+        printf("%d-th time is %f s\n", i, (end.tv_sec - start.tv_sec) + (end.tv_usec - start.tv_usec) / 1000000.);
+    }
+    printf("average time is %f s\n", total_time / 1000000. / cnt);
+    return 0;
+}
+```
+
+测试程序的平均运行时间(单位:s)如下。
+
+|              |    关闭编译优化     |    打开编译优化     |
+| :----------: | :-----------------: | :-----------------: |
+| 关闭内联函数 | 3.197516 (test11.0) | 2.539210 (test11.2) |
+| 打开内联函数 | 2.996570 (test11.1) | 0.034671 (test11.3) |
+
+对于 test11.3,编译器忽略了函数调用,所以循环内部没有语句,相当于循环变量从 1 累加到 100000000,自然速度很快。
+
+对于 test11.2,循环里每次会用两条 `mov` 指令和一条 `callq` 指令调用函数,虽然函数是被优化过的,但还是需要一定时间,所以速度比 test11.3 慢很多。
+
+对于 test11.1,循环里直接嵌入了函数,不需要函数调用,但因为函数没有被优化,所以总体上比 test11.2 慢一些。
+
+对于 test11.0,循环里需要调用函数,而且函数没有被优化,所以速度是最慢的。
+
+以上结果符合预期。
+
+### 有副作用函数
+
+为 sum 函数添加副作用,再次进行测试。函数代码如下。
+
+```go
+var c, d, e, f, g, h, i, j int
+
+func sum(a, b int) int {
+	c = a + b*1
+	d = b - c/2
+	e = c*d + 3
+	f = d/e - 4
+	g = e + f*5
+	h = f - g/6
+	i = g*h + 7
+	j = h/i - 8
+	return j
+}
+```
+
+测试文件为 **_test12.go_** 。生成的目标文件大小(单位:B)如下。
+
+|              |    关闭编译优化     |    打开编译优化     |
+| :----------: | :-----------------: | :-----------------: |
+| 关闭内联函数 | 56576 (test12.0.o)  | 56538 (test12.2.o)  |
+| 打开内联函数 | 774078 (test12.1.o) | 435408 (test12.3.o) |
+
+打开内联函数的目标文件大小远大于关闭内联函数的目标文件,因为每个函数调用都会被替换成函数代码。就算同时打开编译优化和内联函数,因为 sum 函数有副作用(会读写内存),所以这些代码不会被编译器忽略。
+
+打开编译优化的目标文件大小小于关闭编译优化的目标文件,因为优化后函数内部的代码变少了。在打开内联函数的情况下,每个函数调用被替换后的代码都变少了,而且实际上编译器自动计算了要写入内存的值,每次赋值只需要一条 `movq` 指令,所以目标文件大小的减少更明显。
+
+测试文件为 **_test13.go_** 。测试程序的平均运行时间(单位:s)如下。
+
+|              |    关闭编译优化     |    打开编译优化     |
+| :----------: | :-----------------: | :-----------------: |
+| 关闭内联函数 | 3.106696 (test13.0) | 2.611334 (test13.2) |
+| 打开内联函数 | 2.903649 (test13.1) | 0.260347 (test13.3) |
+
+除了同时打开编译优化和内联函数的情况,其他结果都与使用无副作用函数类似,因为有副作用函数与无副作用函数的指令数相差不大。
+
+同时打开编译优化和内联函数时,无副作用函数会被忽略,而有副作用函数不能被忽略,仍需要写内存,所以比无副作用函数慢很多。但由于函数参数是常数,编译器会自动计算每个内存地址应该写入什么值,所以比有副作用函数的其他情况快很多(它们每次都需要重新计算)。
+
+部分汇编代码如下。
+
+```x86asm
+        sum(1, 2)
+  0x384cc               90                      NOPL                                 // nop
+        c = a + b*1
+  0x384cd               48c7050000000003000000  MOVQ $0x3, 0(IP)                     // movq $0x3,(%rip)        [3:7]R_PCREL:"".c+-4
+        d = b - c/2
+  0x384d8               48c7050000000001000000  MOVQ $0x1, 0(IP)                     // movq $0x1,(%rip)        [3:7]R_PCREL:"".d+-4
+        e = c*d + 3
+  0x384e3               48c7050000000006000000  MOVQ $0x6, 0(IP)                     // movq $0x6,(%rip)        [3:7]R_PCREL:"".e+-4
+        f = d/e - 4
+  0x384ee               48c70500000000fcffffff  MOVQ $-0x4, 0(IP)                    // movq $-0x4,(%rip)       [3:7]R_PCREL:"".f+-4
+        g = e + f*5
+  0x384f9               48c70500000000f2ffffff  MOVQ $-0xe, 0(IP)                    // movq $-0xe,(%rip)       [3:7]R_PCREL:"".g+-4
+        h = f - g/6
+  0x38504               48c70500000000feffffff  MOVQ $-0x2, 0(IP)                    // movq $-0x2,(%rip)       [3:7]R_PCREL:"".h+-4
+        i = g*h + 7
+  0x3850f               48c7050000000023000000  MOVQ $0x23, 0(IP)                    // movq $0x23,(%rip)       [3:7]R_PCREL:"".i+-4
+        j = h/i - 8
+  0x3851a               48c70500000000f8ffffff  MOVQ $-0x8, 0(IP)                    // movq $-0x8,(%rip)       [3:7]R_PCREL:"".j+-4
+```
+
+以上结果符合预期。
+
+# 参考文献
+
+1. A Quick Guide to Go's Assembler, https://golang.org/doc/asm.
+2. Git repositories on go, arch.go, https://go.googlesource.com/go/+/master/src/cmd/asm/internal/arch/arch.go.
+3. x64 Architecture, https://docs.microsoft.com/zh-cn/windows-hardware/drivers/debugger/x64-architecture.

+ 400 - 0
source/_posts/coding/go-语言字符串相等函数的-simd-实现.md

@@ -0,0 +1,400 @@
+---
+title: Go 语言字符串相等函数的 SIMD 实现
+tags:
+  - Course Project
+categories:
+  - - Coding
+    - Programming Language
+date: 2021-12-30 17:24:00
+---
+
+# 背景说明
+
+**Go 语言**是 Google 开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。它用批判吸收的眼光,融合 C 语言、Java 等众家之长,将简洁、高效演绎得淋漓尽致。
+
+**SIMD** 全称 Single Instruction Multiple Data,即单指令多数据流,用一条指令对多个数据进行操作。一般用向量寄存器来实现。常用于加速数据密集型运算,如数列求和、矩阵乘法。
+
+本实验对 Go 语言自带的字符串相等函数的源码进行分析,用自己实现的函数进行替换,并比较性能。
+
+# 探索过程
+
+不同 CPU 对 SIMD 的支持不同。用 CPU-Z 查看当前 CPU 支持的 SIMD 指令集。最新的指令集为 **AVX2**。
+
+Go 语言汇编器基于 Plan 9 汇编器的输入风格,与 GNU 汇编器不同。阅读和编写代码时需要注意。
+
+Go 语言字符串相等函数的代码在 `GOROOT/src/internal/bytealg/equal_[arch].s` 文件中。当前 CPU 架构为 AMD64,对应文件为 `equal_amd64.s`。
+
+文件中定义了三个函数:`runtime·memequal`,`runtime·memequal_varlen`,`memeqbody`。前两个函数为 ABI,供应用程序调用。它们将设置相应寄存器并跳转到第三个函数。部分定义如下。
+
+```x86asm
+// memequal(a, b unsafe.Pointer, size uintptr) bool
+TEXT runtime·memequal<ABIInternal>(SB),NOSPLIT,$0-25
+        // AX = a    (want in SI)
+        // BX = b    (want in DI)
+        // CX = size (want in BX)
+        ...
+        JMP     memeqbody<>(SB)
+
+// memequal_varlen(a, b unsafe.Pointer) bool
+TEXT runtime·memequal_varlen<ABIInternal>(SB),NOSPLIT,$0-17
+        // AX = a       (want in SI)
+        // BX = b       (want in DI)
+        // 8(DX) = size (want in BX)
+        ...
+        JMP     memeqbody<>(SB)
+
+// Input:
+//   a in SI
+//   b in DI
+//   count in BX
+// Output:
+//   result in AX
+TEXT memeqbody<>(SB),NOSPLIT,$0-0
+		...
+		RET
+```
+
+我们只需要关注真正进行相等判断的 `memeqbody` 函数。它接收两个字符串的地址(`SI`、`DI`)以及字符串的长度(`BX`),返回 0 或 1(`AX`)。Go 语言的编译器保证这两个字符串的长度相等(否则可以直接判断不相等)。
+
+原代码巧妙地利用了 SIMD。思路如下:
+
+1. 如果字符串的长度不小于 64,则一轮循环比较 64 个字符(512 位),直到剩余长度小于 64;
+2. 如果字符串的长度不小于 8,则一轮循环比较 8 个字符(64 位),直到剩余长度小于 8;
+3. 比较剩余字符(不用循环)。
+
+结合代码分析。在函数开头,根据字符串的长度进入不同的循环。
+
+```x86asm
+TEXT memeqbody<>(SB),NOSPLIT,$0-0
+        CMPQ    BX, $8
+        JB      small
+        CMPQ    BX, $64
+        JB      bigloop
+        CMPB    internal∕cpu·X86+const_offsetX86HasAVX2(SB), $1
+        JE      hugeloop_avx2
+```
+
+第 6 行的代码判断 CPU 是否支持 AVX2 指令集,如果支持则用 `Y` 系列寄存器比较 64 个字符,否则用 `X` 系列寄存器比较 64 个字符。64 个字符的循环如下。
+
+```x86asm
+        // 64 bytes at a time using xmm registers
+hugeloop:
+        CMPQ    BX, $64
+        JB      bigloop
+        MOVOU   (SI), X0
+        MOVOU   (DI), X1
+        MOVOU   16(SI), X2
+        MOVOU   16(DI), X3
+        MOVOU   32(SI), X4
+        MOVOU   32(DI), X5
+        MOVOU   48(SI), X6
+        MOVOU   48(DI), X7
+        PCMPEQB X1, X0
+        PCMPEQB X3, X2
+        PCMPEQB X5, X4
+        PCMPEQB X7, X6
+        PAND    X2, X0
+        PAND    X6, X4
+        PAND    X4, X0
+        PMOVMSKB X0, DX
+        ADDQ    $64, SI
+        ADDQ    $64, DI
+        SUBQ    $64, BX
+        CMPL    DX, $0xffff
+        JEQ     hugeloop
+        XORQ    AX, AX  // return 0
+        RET
+
+        // 64 bytes at a time using ymm registers
+hugeloop_avx2:
+        CMPQ    BX, $64
+        JB      bigloop_avx2
+        VMOVDQU (SI), Y0
+        VMOVDQU (DI), Y1
+        VMOVDQU 32(SI), Y2
+        VMOVDQU 32(DI), Y3
+        VPCMPEQB        Y1, Y0, Y4
+        VPCMPEQB        Y2, Y3, Y5
+        VPAND   Y4, Y5, Y6
+        VPMOVMSKB Y6, DX
+        ADDQ    $64, SI
+        ADDQ    $64, DI
+        SUBQ    $64, BX
+        CMPL    DX, $0xffffffff
+        JEQ     hugeloop_avx2
+        VZEROUPPER
+        XORQ    AX, AX  // return 0
+        RET
+```
+
+如果发现不同,则直接返回 0,否则继续循环,直到剩余长度小于 64,进入 8 个字符的循环。一个通用寄存器刚好可以装下 8 个字符,所以不需要 SIMD。
+
+```x86asm
+bigloop_avx2:
+        VZEROUPPER
+
+        // 8 bytes at a time using 64-bit register
+bigloop:
+        CMPQ    BX, $8
+        JBE     leftover
+        MOVQ    (SI), CX
+        MOVQ    (DI), DX
+        ADDQ    $8, SI
+        ADDQ    $8, DI
+        SUBQ    $8, BX
+        CMPQ    CX, DX
+        JEQ     bigloop
+        XORQ    AX, AX  // return 0
+        RET
+```
+
+最后比较剩余字符。不过这里并没有用循环,而是直接加载字符串末尾的 8 个字符(可能与之前判断过的字符重叠)。
+
+```x86asm
+        // remaining 0-8 bytes
+leftover:
+        MOVQ    -8(SI)(BX*1), CX
+        MOVQ    -8(DI)(BX*1), DX
+        CMPQ    CX, DX
+        SETEQ   AX
+        RET
+```
+
+如果字符串的长度本来就小于 8,这么做会加载一些不属于字符串的字符。代码中对这种情况也做了处理。
+
+可以看出,这个函数尽可能地使用了 SIMD,用一条指令判断多个字符是否相等,加快了处理速度,同时也保证了边界情况下的正确性,对较短的字符串进行特殊处理。
+
+尝试用自己编写的函数进行替换。首先用最简单的方法,直接一个一个字符进行比较。
+
+```x86asm
+TEXT memeqbody<>(SB),NOSPLIT,$0-0
+loop_1:
+        CMPQ    BX, $0
+        JEQ     equal
+        MOVB    (SI), CX
+        MOVB    (DI), DX
+        ADDQ    $1, SI
+        ADDQ    $1, DI
+        SUBQ    $1, BX
+        CMPB    CX, DX
+        JEQ     loop_1
+        XORQ    AX, AX
+        RET
+
+equal:
+        SETEQ   AX
+        RET
+```
+
+代码很短,但显然效率不够高。添加 **8 个字符**的循环,剩余字符还是一个一个比较。
+
+```x86asm
+loop_8:
+        CMPQ    BX, $8
+        JB      loop_1
+        MOVQ    (SI), CX
+        MOVQ    (DI), DX
+        ADDQ    $8, SI
+        ADDQ    $8, DI
+        SUBQ    $8, BX
+        CMPQ    CX, DX
+        JEQ     loop_8
+        XORQ    AX, AX
+        RET
+```
+
+当前 CPU 支持 MMX、SSE4.2、AVX2 指令集。**MMX** 的寄存器为 64 位寄存器,一个寄存器可以装下 8 个字符。将 8 个字符的循环改为使用 MMX 的寄存器。
+
+```x86asm
+loop_8_mmx:
+        CMPQ    BX, $8
+        JB      loop_1
+        MOVQ    (SI), M0
+        MOVQ    (DI), M1
+        PCMPEQB M0, M1
+        MOVQ    M1, CX
+        ADDQ    $8, SI
+        ADDQ    $8, DI
+        SUBQ    $8, BX
+        CMPQ    CX, $-1
+        JEQ     loop_8_mmx
+        XORQ    AX, AX
+        RET
+```
+
+第 4、5 行将从地址 `SI`、`DI` 开始的 8 个字符分别存入 `M0`、`M1` 寄存器,即进行了打包。第 6 行用 `PCMPEQB` 指令(compare packed bytes for equal)比较 `M0`、`M1` 打包字节整数值的相等性,并将比较结果存入 `M1`。如果 `M0`、`M1` 中的某个字节相等,则 `M1` 中的这个字节会变成全 1(即有符号数的 -1),否则变成全 0。因为 MMX 指令不会修改状态寄存器,所以需要将 `M1` 的值存入 `CX`,再与 -1 比较。
+
+**SSE4.2** 的寄存器为 128 位寄存器,一个寄存器可以装下 16 个字符。添加 **16 个字符**的循环。
+
+```x86asm
+loop_16_sse:
+        CMPQ    BX, $16
+        JB      loop_8_mmx
+        MOVUPD  (SI), X0
+        MOVUPD  (DI), X1
+        PCMPEQB X0, X1
+        PMOVMSKB X1, CX
+        ADDQ    $16, SI
+        ADDQ    $16, DI
+        SUBQ    $16, BX
+        CMPW    CX, $0xffff
+        JEQ     loop_16_sse
+        XORQ    AX, AX
+        RET
+```
+
+第 4、5 行用 `MOVUPD` 指令(move two unaligned packed double-precision floating-point values between XMM registers and memory)将从地址 `SI`、`DI` 开始的 16 个字符分别存入 `X0`、`X1` 寄存器。第 6 行将比较结果存入 `X1`。第 7 行用 `PMOVMSKB` 指令(move byte mask)将 `X1` 中每个字节的最高位提取出来存入 `CX` 的低位。这是因为 `X1` 寄存器有 128 位,无法直接存入通用寄存器进行判断。而比较某个字节时,如果相等则这个字节会变成全 1,所以只要提取每个字节的最高位即可判断是否全部相等。
+
+**AVX2** 的寄存器为 256 位寄存器,一个寄存器可以装下 32 个字符。添加 **32 个字符**的循环。
+
+```x86asm
+loop_32_avx:
+        CMPQ    BX, $32
+        JB      loop_16_sse
+        VMOVUPD (SI), Y0
+        VMOVUPD (DI), Y1
+        VPCMPEQB Y0, Y1, Y1
+        VPMOVMSKB Y1, CX
+        ADDQ    $32, SI
+        ADDQ    $32, DI
+        SUBQ    $32, BX
+        CMPL    CX, $0xffffffff
+        JEQ     loop_32_avx
+        XORQ    AX, AX
+        RET
+```
+
+AVX2 指令集与 SSE4.2 指令集类似,只需在指令前加字母 V。唯一不同的是第 6 行的 `VPCMPEQB` 指令,它需要三个操作数,将前两个操作数的比较结果存入第三个操作数。
+
+由于 CPU 不支持 AVX512 指令集,因此无法使用 512 位寄存器。不过,还可以用**循环展开**来优化代码。AVX2 的寄存器有 16 个,所以可以展开 2 次、4 次、8 次循环。以展开 4 次循环为例,添加 **128 个字符**的循环。
+
+```x86asm
+loop_128_unroll:
+        CMPQ    BX, $128
+        JB      loop_32_avx
+        VMOVUPD (SI), Y0
+        VMOVUPD (DI), Y1
+        VMOVUPD 32(SI), Y2
+        VMOVUPD 32(DI), Y3
+        VMOVUPD 64(SI), Y4
+        VMOVUPD 64(DI), Y5
+        VMOVUPD 96(SI), Y6
+        VMOVUPD 96(DI), Y7
+        VPCMPEQB Y0, Y1, Y1
+        VPCMPEQB Y2, Y3, Y3
+        VPCMPEQB Y4, Y5, Y5
+        VPCMPEQB Y6, Y7, Y7
+        VPAND   Y1, Y3, Y3
+        VPAND   Y5, Y7, Y7
+        VPAND   Y3, Y7, Y7
+        VPMOVMSKB Y7, CX
+        ADDQ    $128, SI
+        ADDQ    $128, DI
+        SUBQ    $128, BX
+        CMPL    CX, $0xffffffff
+        JEQ     loop_128_unroll
+        XORQ    AX, AX
+        RET
+```
+
+第 4 行到第 7 行将连续 128 个字符存入 `Y` 系列寄存器,第 12 行到第 15 行分别比较四对 `Y` 系列寄存器,第 16 行到第 18 行将比较结果作按位与(因为相等的比较结果为全 1),第 19 行将结果每个字节的最高位提取出来存入 `CX` 的低位。展开 2 次、8 次循环的代码类似。
+
+至此,已基本实现了用 SIMD 优化的字符串相等函数。
+
+# 效果分析
+
+编写测试函数,用于测试字符串相等函数的正确性。
+
+```go
+func TestEqual(t *testing.T) {
+	d, s := []byte("abcde"), []byte("abcde")
+	if !bytes.Equal(d, s) { // len < 8
+		t.Errorf("Equal(d, s) = false; want true")
+	}
+	for i := 0; i < 50; i++ {
+		d, s = append(d, 'f'), append(s, 'f')
+	}
+	if !bytes.Equal(d, s) { // len < 64
+		t.Errorf("Equal(d, s) = false; want true")
+	}
+	for i := 0; i < 500; i++ {
+		d, s = append(d, 'g'), append(s, 'g')
+	}
+	if !bytes.Equal(d, s) { // len >= 64
+		t.Errorf("Equal(d, s) = false; want true")
+	}
+	d = append(d, 'h')
+	if bytes.Equal(d, s) { // len(d) > len(s)
+		t.Errorf("Equal(d, s) = true; want false")
+	}
+	s = append(s, 'i')
+	if bytes.Equal(d, s) { // len(d) == len(s) && d[len-1] != s[len-1]
+		t.Errorf("Equal(d, s) = true; want false")
+	}
+	s = append(s, 'j')
+	if bytes.Equal(d, s) { // len(d) < len(s)
+		t.Errorf("Equal(d, s) = true; want false")
+	}
+	d, s = []byte("k"), []byte("l")
+	for i := 0; i < 5000; i++ {
+		d, s = append(d, 'm'), append(s, 'm')
+	}
+	if bytes.Equal(d, s) { // len(d) == len(s) && d[0] != s[0]
+		t.Errorf("Equal(d, s) = true; want false")
+	}
+}
+```
+
+用 `go test -run ^TestEqual$` 命令运行测试函数。实验中编写的每一种循环都可以通过测试。
+
+编写基准函数,用于测试字符串相等函数的性能。
+
+```go
+func BenchmarkEqual(b *testing.B) {
+	d, s := []byte(""), []byte("")
+	for i := 0; i < 4096; i++ { // 4KB
+		d = append(d, 'n')
+		s = append(s, 'n')
+	}
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		bytes.Equal(d, s)
+	}
+}
+```
+
+用 `go test -run ^$ -bench ^BenchmarkEqual$ -count 10` 命令运行基准函数,可以得到字符串相等函数的运行次数和平均运行时间。字符串大小均为 4KB。据此可以算出函数处理数据的速率。
+
+在表格的说明中,**R1** 表示一轮比较 1 个字符,**R8** 表示用通用寄存器一轮比较 8 个字符,**M8** 表示用 MMX 寄存器一轮比较 8 个字符,**X16** 表示用 SSE4.2 寄存器一轮比较 16 个字符,**Y32** 表示用 AVX2 寄存器一轮比较 32 个字符,**Y64**、**Y128**、**Y256** 分别表示用 AVX2 寄存器和 2 次、4 次、8 次循环展开一轮比较 64 个、128 个、256 个字符。
+
+|       测试程序        |         说明         | 运行次数 | 运行时间(ns/op) | 处理速率(GB/s) |
+| :-------------------: | :------------------: | :------: | :---------------: | :--------------: |
+|    `original.asm`     |     Go 语言自带      | 13499010 |       87.92       |      43.39       |
+|     `loop_1.asm`      |          R1          |  424518  |       2721        |      1.402       |
+|     `loop_8.asm`      |       R8 + R1        | 3993097  |       298.0       |      12.80       |
+|   `loop_8_mmx.asm`    |       M8 + R1        | 2980017  |       395.4       |      9.648       |
+|   `loop_16_sse.asm`   |    X16 + R8 + R1     | 5688907  |       206.8       |      18.45       |
+|   `loop_32_avx.asm`   |    Y32 + R8 + R1     | 10291144 |       115.2       |      33.11       |
+| `loop_64_unroll.asm`  |    Y64 + R8 + R1     | 13146477 |       88.00       |      43.35       |
+| `loop_128_unroll.asm` | Y128 + Y32 + R8 + R1 | 17871183 |       64.31       |      59.32       |
+| `loop_256_unroll.asm` | Y256 + Y32 + R8 + R1 | 21847575 |       54.56       |      69.92       |
+
+可以看出:
+
+1. 随着 SIMD 寄存器位数的增加,函数的运行时间会减少,处理数据的速率也会变快;
+2. 用通用寄存器比较 8 个字符比用 MMX 寄存器比较 8 个字符要快,是因为 MMX 寄存器比较相等之后还需要移回通用寄存器进行跳转判断;
+3. Go 语言自带的函数用 AVX2 的寄存器时只展开了 2 次循环,实验中展开了 2 次循环的函数与 Go 语言自带的函数速度相近,而展开了 4 次、8 次循环的函数速度更快;
+4. 对于大小为 4KB 的字符串,实验中最快的函数相比最慢的函数速度提升 4887%,相比 Go 语言自带的函数速度提升 61.14%。
+
+以上结果符合预期。
+
+# 参考文献
+
+1. A Quick Guide to Go's Assembler, https://go.dev/doc/asm.
+2. A Manual for the Plan 9 assembler, https://9p.io/sys/doc/asm.html.
+3. x64 Cheat Sheet, https://cs.brown.edu/courses/cs033/docs/guides/x64_cheatsheet.pdf.
+4. x86 and amd64 instruction reference, https://www.felixcloutier.com/x86/index.html.
+5. x86 Assembly Language Reference Manual, https://docs.oracle.com/cd/E37838_01/html/E61064/index.html.
+6. Intel® Instruction Set Extensions Technology, https://www.intel.com/content/www/us/en/support/articles/000005779/processors.html.
+7. equal_amd64.s, https://github.com/golang/go/blob/master/src/internal/bytealg/equal_amd64.s.
+8. testing package, https://pkg.go.dev/testing.

+ 225 - 0
source/_posts/coding/introduction-to-mapreduce.md

@@ -0,0 +1,225 @@
+---
+title: Introduction to MapReduce
+tags:
+  - Course Project
+categories:
+  - - Coding
+    - Back End Development
+date: 2021-07-03 12:34:00
+---
+
+# Introduction
+
+There are tons of large data in today's life. Processing large data on a single computer becomes unacceptable as the scale of data soars. A classic problem is the word count problem: Given very large text data, get the the number of occurrences of each word. If we use multiple computers, we can speed up the process, but the complexity of programming will also increase. MapReduce can help us handle large data on a cluster of computers fast and easily. In fact, MapReduce is an idea of data parallelism, and also a model of data parallelism.
+
+In this article, we are going to implement a serial program and a MapReduce parallel program for the word count problem, and compare their performances.
+
+# Algorithm Specification
+
+## Brief Introduction to MapReduce
+
+MapReduce model consists of two main stages: map stage and reduce stage.
+
+Map stage accepts some input records, and transform them into some intermediate records. Let's take the word count problem as an example. Assume that we are given the following text:
+
+```
+apple is red
+orange is orange
+banana is yellow
+```
+
+Before map stage, the input text is divided into several input splits. Each of them is assigned to a mapper.
+
+```
+("apple is red")		->	Mapper 1
+("orange is orange")	->	Mapper 2
+("banana is yellow")	->	Mapper 3
+```
+
+In map stage, we split the data into several pairs `(key, value)`. In this example, `key` represents for a word and `value` represents for one occurrence of that word.
+
+```
+Mapper 1	->	("apple", 1), ("is", 1), ("red", 1)
+Mapper 2	->	("orange", 1), ("is", 1), ("orange", 1)
+Mapper 3	->	("banana", 1), ("is", 1), ("yellow", 1)
+```
+
+Between map stage and reduce stage, there is a shuffle (or partition) process. The pairs with the same key are merged together, forming pairs like `(key, <set of values>)`, and are sent to the same reducer.
+
+```
+("apple", <1>), ("orange", <1, 1>)	->	Reducer 1
+("is", <1, 1, 1>), ("banana", <1>)	->	Reducer 2
+("red", <1>), ("yellow", <1>)		->	Reducer 3
+```
+
+In reduce stage, we reduce a set of intermediate values which share a key to a smaller set of values. In this example, we simply calculate the sum of values in the set for each key.
+
+```
+Reducer 1	->	("apple", 1), ("orange", 2)
+Reducer 2	->	("is", 3), ("banana", 1)
+Reducer 3	->	("red", 1), ("yellow", 1)
+```
+
+After reduce stage, we merge the results generated by reducers together.
+
+```
+("apple", 1)
+("orange", 2)
+("is", 3)
+("banana", 1)
+("red", 1)
+("yellow", 1)
+```
+
+And here we have finished the task, i.e., find the number of occurrences of each word.
+
+## Brief Introduction to Hadoop
+
+Despite simple principle, MapReduce isn't that easy to implement. There are lots of details to consider, such as how to transmit data between computers, how to split the input, how to manage memory and when to write data to disk. Fortunately, Apache Software Foundation has an excellent implementation called Hadoop. We can just focus on map function and reduce function, and deploy Hadoop on a cluster of computers. The Hadoop framework will handle the rest of things. Specifically, Hadoop implements a file system called HDFS to store files and read and write data during MapReduce process.
+
+Let's see how to use Hadoop to solve the word count problem. First, we need to build a cluster of computers with Hadoop installed. We use Docker because it is convenient to deploy several containers from the same image. Then, we should specify one of the containers to be name node (manage HDFS), and the other containers to be data nodes (store data). After that, we put the test data into HDFS, and write our own map function and reduce function. Finally, we compile the program with Hadoop library and run it using Hadoop to get the result.
+
+To be more concrete, the map function should be defined in a subclass of `Mapper`. We define class `TokenizerMapper` that extends `Mapper<Object, Text, Text, IntWritable>`. Here, the first two parameters indicate the type of input pair of map function (`<Object, Text>`), and the last two parameters indicate the type of output pair of map function (`<Text, IntWritable>`). We define a word as a string that contains only letters and quotation marks and doesn't start with or end with quotation mark. So we can write our map function like this:
+
+```java
+public static class TokenizerMapper extends Mapper<Object, Text, Text, IntWritable> {
+    private final static IntWritable one = new IntWritable(1);
+    private Text word = new Text();
+
+    /* map function */
+    public void map(Object key, Text value, Context context) throws IOException, InterruptedException {
+        String line = value.toString();
+        String str = new String("");
+        for (int i = 0; i < line.length(); i++) {
+            if (Character.isLetter(line.charAt(i)) || str.length() > 0 && line.charAt(i) == '\'') {
+                str += Character.toLowerCase(line.charAt(i)); // extend word
+            } else {
+                for (; str.length() > 0 && str.charAt(str.length() - 1) == '\'';) {
+                    str = str.substring(0, str.length() - 1); // remove trailing quotation mark
+                }
+                if (str.length() > 0) {
+                    word.set(str);
+                    context.write(word, one); // output pair
+                    str = "";
+                }
+            }
+        }
+        for (; str.length() > 0 && str.charAt(str.length() - 1) == '\'';) {
+            str = str.substring(0, str.length() - 1); // remove trailing quotation mark
+        }
+        if (str.length() > 0) {
+            word.set(str);
+            context.write(word, one); // output pair
+        }
+    }
+}
+```
+
+Similarly, the reduce function should be defined in a subclass of `Reducer`. We define class `IntSumReducer` that extends `Reducer<Text, IntWritable, Text, IntWritable>`. Here, the first two parameters indicate the type of input pair of reduce function (`<Text, IntWritable>`), which should be equal to the type of output pair of map function, and the last two parameters indicate the type of output pair of reduce function (`<Text, IntWritable>`). Note that by default, the output pairs are sorted by their `key`, i.e., by the word in lexicographical order. If we want to customize the order, we need to do extra work after reducing. We can store the pairs and do not output them in reduce function. Instead, we sort and output them in cleanup function, which will be executed after all reducers finish their work. We can write our reduce function like this:
+
+```java
+public static class IntSumReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
+    private HashMap<String, Integer> record = new HashMap<String, Integer>(); // store count in map, used for final sort
+    private Text word = new Text();
+    private IntWritable result = new IntWritable();
+
+    /* reduce function */
+    public void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
+        int sum = 0;
+        for (IntWritable val : values) {
+            sum += val.get();
+        }
+        record.put(key.toString(), record.getOrDefault(key.toString(), 0) + sum); // update map, don't output here
+    }
+
+    /* cleanup function executed after reducing */
+    public void cleanup(Context context) throws IOException, InterruptedException {
+        ArrayList<HashMap.Entry<String, Integer>> list = new ArrayList<HashMap.Entry<String, Integer>>(record.entrySet()); // get answer from map
+        Collections.sort(list, new Comparator<HashMap.Entry<String, Integer>>() {
+            public int compare(HashMap.Entry<String, Integer> o1, HashMap.Entry<String, Integer> o2) { // compare function
+                if (o1.getValue().equals(o2.getValue())) {
+                    return o1.getKey().compareTo(o2.getKey());
+                }
+                return o1.getValue() < o2.getValue() ? 1 : -1;
+            }
+        });
+        for (HashMap.Entry<String, Integer> itr : list) {
+            word.set(itr.getKey());
+            result.set(itr.getValue());
+            context.write(word, result); // output here
+        }
+    }
+}
+```
+
+## A Simple Implementation of MapReduce
+
+If we just want to process relatively small data on a multicore computer using parallelism, we can implement our own MapReduce framework. We choose Golang as our programming language because it is easy to reach high concurrency. More importantly, the communication between different threads is very convenient, using a special type in Golang called channel.
+
+In Golang, we can start a new thread simply using `go funcName(args)`. And we can define a channel of string using `chanName chan string`. To send something via channel, use `chanName <- something`. To receive something from channel, use `something := <-chanName`. Thus, we are able to communicate with other threads using `go funcName(inChan, outChan)`, and send data via `inChan` and receive data from `outChan`.
+
+To implement map stage, for simplicity, we just split the input data by file (that is, each file will be assigned to a mapper to be processed). We can write mapper like this:
+
+```go
+/* Frame-defined mapper */
+func Mapper(config Config, filename chan string, shuffle chan Pair) {
+	output := make(chan Pair, chanSize)
+	merge := make(map[string][]int)
+	for name := range filename {
+		text, _ := os.ReadFile(name) // read file content
+		go config.mapFunc(text, output)
+		for pair := range output {
+			if pair.key == "" { // map finish
+				break
+			}
+			merge[pair.key] = append(merge[pair.key], pair.value) // merge in map stage
+		}
+	}
+	for key, list := range merge {
+		shuffle <- config.combineFunc(key, list) // combine local result, to shuffle stage
+	}
+	shuffle <- Pair{"", 0} // mapper finish
+}
+```
+
+Here, we merge and combine local result in map stage. This is because we can send `(key, 3)` once instead of sending `(key, 1)` three times, to reduce data transmission and lighten the work of reducer.
+
+To implement shuffle process, we simply use a map to ensure that pairs with the same key are sent to the same reducer. We can write shuffle process like this:
+
+```go
+merge := make(map[string][]int)
+for pair := range shuffle { // get from map stage
+    if pair.key == "" {
+        workerCnt--
+        if workerCnt == 0 {
+            break
+        }
+    } else {
+        merge[pair.key] = append(merge[pair.key], pair.value) // merge in shuffle stage
+    }
+}
+```
+
+To implement reduce stage, we just call reduce function. We can write reduce stage like this:
+
+```go
+/* Frame-defined reducer */
+func Reducer(config Config, keylist chan AggPair, collect chan Pair) {
+	for aggPair := range keylist {
+		collect <- config.reduceFunc(aggPair.key, aggPair.list) // to final stage
+	}
+	collect <- Pair{"", 0} // reducer finish
+}
+```
+
+## Serial Algorithm
+
+We are given a set of documents. We can read them one by one, and scan the content to find words. Use a map (not the map in MapReduce) to store the number of occurrences of each word. After processing all documents, we've got the answer, and we just need to sort them in specified order.
+
+# Analysis and Comments
+
+The main limitation is the hardware. For a single computer, if it has more threads, then it can run more mappers or reducers simultaneously, which will increase the speed. For a cluster of computers, increase the number of computers means more nodes, and it will also increase the speed.
+
+When we increase the number of documents, the serial program surely needs more time to get the result. For the parallel program, if we do not increase the number of mappers, it will run slower because each mapper takes more time to finish. Otherwise, the running time of program will roughly remain invariant because the time for map stage will not change (we have more mappers), so as the time for reduce stage (the number of different words will possibly change very slightly).
+
+In short, the larger the data we want to process, the more efficient the parallel program compared to the serial program.