這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
Java SIMD Lucene Elasticsearch
我們首先來看一下 JAVA 如何使用 CPU 的 SIMD 指令。這是一個ru的哥們嘗試在lucene裡使用SIMD指令加速lucene的postings list(也就是指定term對應的文檔id列表)的解碼:
http://blog.griddynamics.com/2015/02/proposing-simd-codec-for-lucene.h...
https://www.youtube.com/watch?v=2HQdbpgHfnQ&index=15&list=PLq-...
最重要的結論就是 java 自身還不支援JIT(運行時產生的機器碼)出SIMD指令。如果用 c/asm 編寫 SIMD 的代碼,在 java 裡調用的話 JNI 本身的開銷抵消了 SIMD 帶來的好處。所以最終需要使
用一種更底層的方式訪問 native 代碼:
http://stackoverflow.com/questions/24746776/what-does-a-jvm-have-to-do...
值得一提的是 elasticsearch 從 2.0 大幅加強了 aggregation,現在已經開始支援 pipeline 了。可以寫出類似 select sum(money) / sum(users_count) from payment 之類的代碼了。自然 SIMD 的最佳化也可以做到 aggregation 階段裡去。
https://www.elastic.co/guide/en/elasticsearch/reference/master/search-...
Go CGO
CGO 慢,顯而易見。
https://github.com/golang/go/blob/master/src/runtime/cgocall.go
具體來說就是這幾行
/* * Announce we are entering a system call * so that the scheduler knows to create another * M to run goroutines while we are in the * foreign code. * * The call to asmcgocall is guaranteed not to * split the stack and does not allocate memory, * so it is safe to call while "in a system call", outside * the $GOMAXPROCS accounting. */ entersyscall(0) errno := asmcgocall(fn, arg) exitsyscall(0)
每次調用 c 的函數都假設了這個函數是阻塞的。entersyscall 會儲存當前協程的堆棧資訊。所以Go的策略和Java一樣,通過讓JNI很慢,迫使使用者把儘可能多的代碼都寫到Go裡。
Go Plan9 Assembly
Go有兩個編譯器,一個是gc(go compiler),一個是gccgo(用的是gcc的後端)。gc編譯器是把代碼從go編譯成plan 9的彙編。plan 9的彙編不是平台無關的,而是每個平台有一個版本,然後和這
個平台本身的彙編文法又有不同。
首先我們可以來看一下 gc 編譯器是不是會產生 SIMD 指令:
https://github.com/golang/go/blob/master/src/cmd/compile/internal/amd6...
可以看到,在這個列表裡是沒有 ADDPD 這樣的 SIMD 指令的。說明 gc 編譯器目前還不支援把普通的加法編譯成向量加法。用 Intel 的編譯器,如果把代碼協程 struct of array 的形式而不是
array of struct 形式的話,編譯器可以自動做向量化最佳化。顯然 gc 編譯器還沒有把這個做為一個最佳化方向。
https://software.intel.com/sites/default/files/8c/a9/CompilerAutovecto...
雖然gc編譯器不支援 SIMD,但是其 plan9 的 assembler 是支援在 amd64 的 SIMD 指令的。
https://github.com/golang/go/blob/master/src/cmd/internal/obj/x86/asm6...
其中有 AADDPD (也就是 ADDPD)。而 Go 是支援在代碼裡混用 go 和 plan9 彙編的。所以 gonum 這個項目就寫了一些 plan9 彙編來最佳化效能:
https://github.com/gonum/internal/blob/master/asm/ddot_amd64.s
簡單做了一個benchmark:
package mainimport "fmt"import "simd/asm"import "testing"func BenchmarkFunction(b *testing.B) { x := make([]float64, 10000) for i := 0; i < len(x); i++ { x[i] = float64(i) } y := make([]float64, 10000) for i := 0; i < len(y); i++ { y[i] = float64(i) } for i := 0; i < b.N; i++ { _ = asm.DdotUnitary(x, y) }}func main() { br := testing.Benchmark(BenchmarkFunction) fmt.Println(br)}
使用 SIMD 版本的點乘,速度為 4616 ns/op。使用非 SIMD 版本的點乘,速度為 12340 ns/op。目前 Go 並不支援 inline plan9 的彙編代碼。也就是彙編寫的函數每次調用都要付出一個函數call
的成本,也就是沒法當成 SIMD intrinsics 那樣來用。不過仍然比 java 強多了……
GCCGO
Go還有另外一個編譯器。它提供了另外一種Cgo的方式,extern。
https://golang.org/doc/install/gccgo
使用 extern 可以把任意的 c 的代碼連結到 go 代碼裡來。至於 scheduler 和 garbage collector 這些就自己好自為之了。甚至類型互相轉換的細節都還是 subject to change 的。可以把它理解
為去掉了安全保護的 cgo。
利用這條路也可以把 SIMD 指令連結到 go 代碼裡來使用:
http://stackoverflow.com/questions/2951028/is-it-possible-to-include-i...
使用 gccgo 可能還可以把這些 SIMD 調用在link時做inline:
https://groups.google.com/forum/#!topic/golang-nuts/kGgkcOFCBtc
https://groups.google.com/forum/#!topic/golang-nuts/TqMTWdYGKOk
引用一段
Answering specifically about gccgo. Gccgo is of course just a frontend to GCC. GCC can not inline functions written in pure assembly. However, GCC provides CPU-specific builtin functions usable in C/C++ for many things that people want to do (e.g., vector instructions) and it also provides a sophisticated asm expression as a C/C++ extension. This means that you can write your assembly code in extended C/C++ instead, and a function written that way can be inlined. It can even be inlined into Go code if you use LTO (link-time optimization, see GCC's -flto options).
總結
Go有三種調用native的代碼的方式:
- cgo
- plan9 assembly
- gccgo extern
相比Java的JNI來說,可選項更多。不遠的將來 go 可以在 spark/lucene 這兩個領域從速度上超過 Java。
go 1.5 的編譯器已經是用 go 寫的。也許將來 go 的編譯器可以和 Intel 的編譯器一樣,自動產生向量化的代碼。