算法競(jìng)賽題目中 Golang IO 方法及其火焰圖對(duì)比
這里不討論過于 hacky 的方法。對(duì) IO 速度極致要求(甚至在數(shù)學(xué)上進(jìn)行優(yōu)化)可參考:
本文討論 5\n1 2 3 4 5E 這類格式的 Stdin 。具體對(duì)比的三種讀入和兩種輸出如下。
package main
import (
"bufio"
"fmt"
"os"
"runtime/pprof"
"strconv"
)
func main() {
f, _ := os.Create("3-2")
defer f.Close()
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
// n, a := read_stdin_1()
// n, a := read_stdin_2()
n, a := read_stdin_3()
quick_sort(a, 0, n-1)
// write_stdout_1(n, a)
write_stdout_2(n, a)
}
// 使用 fmt.Scanf 讀入
func read_stdin_1() (int, []int) {
var n int
fmt.Scanf("%d", &n)
a := make([]int, n)
for i := 0; i < n; i++ {
fmt.Scanf("%d", &a[i])
}
return n, a
}
// 使用 bufio.NewReader 讀入
func read_stdin_2() (int, []int) {
in := bufio.NewReader(os.Stdin)
var n int
fmt.Fscan(in, &n)
a := make([]int, n)
for i := 0; i < n; i++ {
fmt.Fscan(in, &a[i])
}
return n, a
}
// 使用 bufio.NewScanner 讀入
func read_stdin_3() (int, []int) {
scanner := bufio.NewScanner(os.Stdin)
scanner.Split(bufio.ScanWords)
var n int
scanner.Scan()
n, _ = strconv.Atoi(scanner.Text())
a := make([]int, n)
for i := 0; i < n; i++ {
scanner.Scan()
a[i], _ = strconv.Atoi(scanner.Text())
}
return n, a
}
// 使用 fmt.Printf 輸出
func write_stdout_1(n int, a []int) {
for i := 0; i < n; i++ {
fmt.Printf("%d ", a[i])
}
}
// 使用 bufio.NewWriter ,手動(dòng) Flush 輸出
func write_stdout_2(n int, a []int) {
out := bufio.NewWriter(os.Stdout)
defer out.Flush()
for i := 0; i < n; i++ {
if i > 0 {
out.WriteByte(' ')
}
out.WriteString(strconv.Itoa(a[i]))
}
}
測(cè)試結(jié)果如下:
read | write | time |
1 | 1 | 5265ms |
2 | 1 | 1435ms |
3 | 1 | 976ms |
1 | 2 | 5156ms |
2 | 2 | 452ms |
3 | 2 | 145ms |
測(cè)試平臺(tái):
先說結(jié)論,再看火焰圖
1. 為什么 bufio.NewReader(os.Stdin) + fmt.Fscan 比 fmt.Scanf 快?
(一)每次 fmt.Scanf 都是一次系統(tǒng)調(diào)用 Syscall ,而 bufio.NewReader(os.Stdin) 是在內(nèi)存中緩存了 bufio 的 buf
(二)doScanf 每次會(huì)去解析 %d ,而 fmt.Fscan(in, &a[i]) 是根據(jù)類型的反射去直接找對(duì)對(duì)應(yīng)的 Scan + Parse 函數(shù),如下
// 這里的 arg 就是我們傳進(jìn)來的 &n 和 &a[i]
func (s *ss) scanOne(verb rune, arg any) {
s.buf = s.buf[:0]
var err error
// If the parameter has its own Scan method, use that.
if v, ok := arg.(Scanner); ok {
err = v.Scan(s, verb)
if err != nil {
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
s.error(err)
}
return
}
switch v := arg.(type) {
...
case *int:
*v = int(s.scanInt(verb, intBits))
...
2. 為什么 bufio.NewScanner + strconv.Atoi 比 bufio.NewReader 快?
(一)bufio.NewScanner 直接使用 scanner.Split(bufio.ScanWords) 中的邏輯掃描輸入流
(二)strconv.Atoi 無需做任何反射,其只實(shí)現(xiàn) string 到 int 的轉(zhuǎn)換。任務(wù)越細(xì)致,越好做優(yōu)化
3. 為什么 bufio.NewWriter + strconv.Itoa 比 fmt.Printf 快?
(一)顯而易見,有 buf 減少了系統(tǒng)調(diào)用 Syscall 的次數(shù)
(二)手動(dòng)控制 Flush ,可以減少系統(tǒng)調(diào)用 Syscall 的次數(shù)
(三)strconv.Itoa 同理無需反射或解析 %d
來看火焰圖 Flame Graph
火焰圖測(cè)試數(shù)據(jù):
- n = 1e6
- 整數(shù)取值范圍:[1, 1e9]
read 1
read 2
read 3
如上,方法二方法三 Syscall 占比小非常多。
write 1
write 2