[TOC]
我們用 Go 寫兩個遍歷兩層 slice 的算法。
~~~
var items = make([][]int32, 1000)
func init() {
for i := 0; i < 1000; i++ {
items[i] = make([]int32, 1000)
for j := 0; j < 1000; j++ {
items[i][j] = rand.Int31n(2)
}
}
}
// 橫向遍歷
func sumRows() int {
var sum = 0
for i := 0; i < 1000; i++ {
for j := 0; j < 1000; j++ {
sum += int(items[i][j])
}
}
return sum
}
// 縱向遍歷
func sumCols() int {
var sum = 0
for i := 0; i < 1000; i++ {
for j := 0; j < 1000; j++ {
sum += int(items[j][i])
}
}
return sum
}
~~~
sumRows() 函數橫向遍歷數組,sumCols() 函數縱向遍歷數組,每個數組計算的次數都是一樣的,所以按道理說兩者的消耗時間也是一樣的。 我們寫兩個基準測試
~~~
func BenchmarkRows(b *testing.B) {
for i := 0; i < b.N; i++ {
sumRows()
}
}
func BenchmarkCols(b *testing.B) {
for i := 0; i < b.N; i++ {
sumCols()
}
}
~~~
跑基準測試
~~~
> $ go test -bench=.s -run=^1 -benchmem
goos: darwin
goarch: amd64
pkg: github.com/yushuailiu/go-algorithm/golang
BenchmarkRows-4 1000 1210319 ns/op 0 B/op 0 allocs/op
BenchmarkCols-4 100 13451343 ns/op 0 B/op 0 allocs/op
PASS
ok github.com/yushuailiu/go-algorithm/golang 2.746s
~~~
我們可以看到縱向遍歷的時間是橫向遍歷的10幾倍。
CPU高速緩存
CPU是執行所有的運算和程序,內存是存放數據和代碼,當CPU 需要數據的時候會去內存取,CPU 做了一個緩存架構,當 CPU 需要數據的時候需要先從緩存中取,取不到再去內存取。

CPU 高速緩存分為3層L1、L2、L3,L1 最快最小 L3 最大最慢。
各級緩存大小及速度
| CPU訪問 | 消耗的CPU時鐘周期 | 大約需要的時間 | 大小 |
| --- | --- | --- | --- |
| 主存 | | 約60~80ns | |
| QPI總線傳輸 | | 約20ns | |
| L3 cache | 約40-45 cycles | 約15ns | 3MB |
| L2 cache | 約10 cycles | 約3ns | 256KB |
| L1 cache | 約3-4 cycles | 約1ns | 32KB |
| 寄存器 | 1cycles | | |
根據表格可以看出如果命中L1 cache那么會比到內存取數據快近兩個數量級。
# 緩存行
高速緩存是由很多 Cache Line 組成的。Cache Line 是 cache 和 RAM(內存)最小數據交換單元,一般大小為 64byte。當 CPU 把內存中的數據載入 cache 時,會把臨近的 64byte 的數據一同寫入 Cache line(空間局限性原則:臨近的數據將來被訪問的可能性很大)。
# 揭秘
大家看了上面段的說明應該有所明白了,是的 橫向遍歷為什么會比縱向遍歷快就是因為橫向遍歷會大量中高速緩存,因為我們知道 Go 中 slice 底層是數組,而數組所有數據是類型相同大小相同連續的內存空間,所以當我們依次訪問一個 slice 的各個元素的時候就會中Cache Line,因為當我們訪問位置為 0 的元素的時候,包括該元素在內以及往后的 64 byte的數據都會載入 Cache Line。
**第一層先訪問,那就把第一層先載入緩存**
Cpu 緩存主要是用來緩存代碼的,數據也是補充.
代碼大部分時候是順序執行的,很多編譯優化也是把條件跳轉,循環跳轉,函數跳轉變成順序代碼
- 基礎
- 簡介
- 主要特征
- 變量和常量
- 編碼轉換
- 數組
- byte與rune
- big
- sort接口
- 和mysql類型對應
- 函數
- 閉包
- 工作區
- 復合類型
- 指針
- 切片
- map
- 結構體
- sync.Map
- 隨機數
- 面向對象
- 匿名組合
- 方法
- 接口
- 權限
- 類型查詢
- 異常處理
- error
- panic
- recover
- 自定義錯誤
- 字符串處理
- 正則表達式
- json
- 文件操作
- os
- 文件讀寫
- 目錄
- bufio
- ioutil
- gob
- 棧幀的內存布局
- shell
- 時間處理
- time詳情
- time使用
- new和make的區別
- container
- list
- heap
- ring
- 測試
- 單元測試
- Mock依賴
- delve
- 命令
- TestMain
- path和filepath包
- log日志
- 反射
- 詳解
- plugin包
- 信號
- goto
- 協程
- 簡介
- 創建
- 協程退出
- runtime
- channel
- select
- 死鎖
- 互斥鎖
- 讀寫鎖
- 條件變量
- 嵌套
- 計算單個協程占用內存
- 執行規則
- 原子操作
- WaitGroup
- 定時器
- 對象池
- sync.once
- 網絡編程
- 分層模型
- socket
- tcp
- udp
- 服務端
- 客戶端
- 并發服務器
- Http
- 簡介
- http服務器
- http客戶端
- 爬蟲
- 平滑重啟
- context
- httptest
- 優雅中止
- web服務平滑重啟
- beego
- 安裝
- 路由器
- orm
- 單表增刪改查
- 多級表
- orm使用
- 高級查詢
- 關系查詢
- SQL查詢
- 元數據二次定義
- 控制器
- 參數解析
- 過濾器
- 數據輸出
- 表單數據驗證
- 錯誤處理
- 日志
- 模塊
- cache
- task
- 調試模塊
- config
- 部署
- 一些包
- gjson
- goredis
- collection
- sjson
- redigo
- aliyunoss
- 密碼
- 對稱加密
- 非對稱加密
- 單向散列函數
- 消息認證
- 數字簽名
- mysql優化
- 常見錯誤
- go run的錯誤
- 新手常見錯誤
- 中級錯誤
- 高級錯誤
- 常用工具
- 協程-泄露
- go env
- gometalinter代碼檢查
- go build
- go clean
- go test
- 包管理器
- go mod
- gopm
- go fmt
- pprof
- 提高編譯
- go get
- 代理
- 其他的知識
- go內存對齊
- 細節總結
- nginx路由匹配
- 一些博客
- redis為什么快
- cpu高速緩存
- 常用命令
- Go 永久阻塞的方法
- 常用技巧
- 密碼加密解密
- for 循環迭代變量
- 備注
- 垃圾回收
- 協程和纖程
- tar-gz
- 紅包算法
- 解決golang.org/x 下載失敗
- 逃逸分析
- docker
- 鏡像
- 容器
- 數據卷
- 網絡管理
- 網絡模式
- dockerfile
- docker-composer
- 微服務
- protoBuf
- GRPC
- tls
- consul
- micro
- crontab
- shell調用
- gorhill/cronexpr
- raft
- go操作etcd
- mongodb