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 main

import "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 的编译器一样,自动产生向量化的代码。