Skip to content

Commit 8d80f11

Browse files
authored
Merge pull request #1749 from future-architect/feature
Go1.26 SIMD
2 parents 806d3c8 + ded2258 commit 8d80f11

3 files changed

Lines changed: 242 additions & 0 deletions

File tree

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
---
2+
title: "Go 1.26連載:インデックス+SIMD"
3+
date: 2026/01/27 00:00:00
4+
postid: a
5+
tag:
6+
- Go
7+
- Go1.26
8+
category:
9+
- Programming
10+
thumbnail: /images/2026/20260127a/thumbnail.jpg
11+
author: 澁川喜規
12+
lede: "Go 1.26のリリースの足音が聞こえてきたので[1.26]の新機能のうち、気になる機能を有志で紹介していく連載を行います。"
13+
---
14+
<img src="/images/2026/20260127a/top.jpg" alt="" width="615" height="615">
15+
16+
Go 1.26のリリースの足音が聞こえてきたので[1.26](https://go.dev/doc/go1.26)の新機能のうち、気になる機能を有志で紹介していく連載を行います。
17+
18+
| Date | Title | Author |
19+
| :--: | :--------------- | :--------- |
20+
| 1/27 | インデックス+SIMDこの記事 | 渋川 |
21+
| 1/28 | goroutine leak profiler | 武田さん |
22+
| 1/29 | runtime/secret | 島ノ江さん |
23+
| 1/30 | go/fix | 真野さん |
24+
| 2/2 | GCの改善 | 棚井さん |
25+
| 2/3 | new(プリミティブ) | 辻さん |
26+
| 2/4 | CGo呼び出し高速化 | 宮崎さん |
27+
28+
# 1.26の新機能のダイジェスト
29+
30+
久々に結構大きめな変更が多くなっていますね
31+
32+
* new(プリミティブ)
33+
* 1.25で導入されたGCが有効化
34+
* cgo, memory allocate高速化
35+
* goroutine leak profiler
36+
* compilerがスライスをよりスタックに割り当てられるように
37+
* SIMDを扱う実験的パッケージ(AMD64のみサポート)
38+
* runtime/secretという機密情報を扱う実験的パッケージ
39+
* image/jpegが高速に
40+
* io.ReadAllの消費メモリが少なく
41+
* log/slogで複数の出力先に一気に出力できるMultiHandlerが追加
42+
* testing.ArtifactDir
43+
* 暗号アルゴリズムの更新たくさん
44+
45+
全体的に高速化が行われています。なのでアップデートするだけで恩恵がいろいろあります。
46+
47+
テストのArtifactDir()は`-artifacts`フラグを付けてテストを実行すると、`T.ArtifactDir()`メソッドが中間生成物の置き場を用意してくれて、そこに格納することであとから検死できるようになるというものです。[以前、TempDir()が使いにくい](https://future-architect.github.io/articles/20241016a/)というのを技術ブログで書きましたが、それのソリューションが提供された感じですね。
48+
49+
# SIMD
50+
51+
それでは、今回追加された[`simd/archsimd`パッケージ](https://pkg.go.dev/simd/archsimd@go1.26rc2)について紹介します。
52+
53+
SIMDというのは1つの命令で複数のデータをまとめて処理する命令セットです。4つ、8つ・・・のデータをまとめて足し算したり、掛け算したり。行列計算などを高速に行うのに使います。
54+
55+
インテル系では主に以下のようにSIMDの命令セットの世代があります。それぞれとレジスタサイズの対応は以下の通りです。
56+
57+
* MMX: 64ビット
58+
* SSE: 128ビット
59+
* AVX: 256ビット
60+
* AVX512: 512ビット
61+
62+
なお、AVX512はそれほど大量のデータを処理する必要とするアプリケーションが一部に限られるせいか、10th/11th Gen Core CPUでは対応していましたが、その後のチップからは取り除かれてしまいました。それだけ半導体を使うのに使用されないのであればその分通常のコア数を増やそうという感じですね。Eコア・Pコアに分かれた時代からは使えなくなりました。あとはCPUだけが早くてもメモリ転送がボトルネックになりそうですしね。
63+
64+
など、上記の命令セットの名前はインテルですが、AMD、ARM(NEON)なども備えています。
65+
66+
例えば、AVXは256ビットですが、これを8ビットx32本、と使ったり、16ビットx16本として使ったり色々できます。[simd/archsimd](https://pkg.go.dev/simd/archsimd)パッケージを見ると、いろんな型がありますが、よく見ると、Float32x8といった型になっております。たくさんありますが、データ型と要素数の組み合わせになっております。合計のビット数は128ビット、256ビット、512ビットがあることがわかります。だいたい、和だったり、どのような演算をするかはメソッドで提供されていますが、int8/uint8などは和はあるが、積などはないなど、型によって使えるメソッドに違いがあります。
67+
68+
なお、Goがバイナリを最後に生成するのに使うアセンブラは、アーキテクチャ限定でSIMD命令にはいくつか対応していたので、コードを.goではなく、.sで作成すればSIMDは今までも活用できました。Go 1.24で導入されたmapの新アルゴリズムの[Swiss Table](https://go.dev/blog/swisstable)は、SIMDを活用して高速検索を行うというものでした。サードパーティのライブラリでSIMDを使った行列演算ライブラリなどもいくつかありましたが、それが今回公式でできるようになりました。
69+
70+
# ベクトルの内積計算
71+
72+
データ作成は後で説明するのですが、embedding化で作ったドキュメントのベクトルデータと、検索用語のベクトルデータの内積を取ることで、どれだけそれぞれの内容が近いかを数値化できます。ベクトル化すると、例えば「猫」について説明した文章があったとして、猫に近い「動物」や「トラ」といった言葉では距離が近いと判定されます。それにより、類似文書検索ができます。転置インデックスを使った全文検索だと「猫」で検索しないとヒットしませんが、ベクトル検索では近い内容も探せます。その分、ベクトルを計算するコーパスの質が大事で、今まではなかなか自由に簡単に使えるものがなかったのですが、生成AIのおかげでモデルを簡単にインストールしたり利用できるようになったので、簡単にベクトル検索ができる時代になりました。
73+
74+
まず、ollamaをインストールして、今回のembeddingで使うモデルを使えるようにしてローカルサーバーを起動しておきます。
75+
76+
```bash
77+
ollama pull nomic-embed-text
78+
$ ollama serve
79+
```
80+
81+
今回実験で使ったnomic-embed-textという軽量なモデルを使うと、768次元のベクトルとしてベクトルが計算できます。SIMDがなかったら、ループを回してそれぞれの項の積をしてから和を計算します。SIMDを使う場合、128ビットだと16ビットの要素の積を8つ同時に行えますし、内積用のメソッドを使うと和も求められます。
82+
83+
ベンチマークを取ってみると、6.6倍ほど高速になっていることがわかります。効果バツグンですね。なお、AMD64しか対応していないので、M3のMacBook AirでAMD64ビルドしたものをRosetta2でテストしています。そのうち早くネイティブ対応して欲しいですね。
84+
85+
なお、Rosettaのエミュレーションが公式には128ビットまでということでその範囲のテストになっていますが、非公式に256ビットとかも行けます。ただ、実行してみると128ビットの2倍程度の時間がかかっているので128ビット演算器で256ビットのエミュレーションをしている感じでしょうか?
86+
87+
```txt
88+
% GOARCH=amd64 GOEXPERIMENT=simd go test -bench . -benchmem
89+
goos: darwin
90+
goarch: amd64
91+
pkg: simdtest
92+
cpu: VirtualApple @ 2.50GHz
93+
BenchmarkPrimitive_RealData-8 5915974 203.0 ns/op 0 B/op 0 allocs/op
94+
BenchmarkSIMD_RealData-8 39017252 29.33 ns/op 0 B/op 0 allocs/op
95+
```
96+
97+
以下のコードがテストです。データの読み込みは省略しています。SIMDの方は[8]int16などの固定長配列を前提としているので、[][8]int16という形で、ベクトルを計算単位でまとめた二重配列的な構造にしてから与えています。なお、テストデータはReal World HTTPの原稿で、「クッキーのセキュリティ」という検索用語をそれぞれベクトル化してあらかじめファイルにしてあり、それを利用しています。
98+
99+
```go
100+
package main
101+
102+
import (
103+
"encoding/json"
104+
"log"
105+
"os"
106+
"simd/archsimd"
107+
"testing"
108+
)
109+
110+
// プリミティブな内積計算ロジック
111+
func dotProductPrimitive(a, b []int16) int32 {
112+
var sum int32 = 0
113+
for i := 0; i < len(a); i++ {
114+
sum += int32(a[i]) * int32(b[i])
115+
}
116+
return sum
117+
}
118+
119+
// SIMDの内積計算ロジック
120+
func dotProductSIMD(a, b [][8]int16) int32 {
121+
acc := archsimd.Int32x4{}
122+
for i := 0; i < len(a); i++ {
123+
vq := archsimd.LoadInt16x8(&a[i])
124+
vt := archsimd.LoadInt16x8(&b[i])
125+
dot := vq.DotProductPairs(vt)
126+
acc = acc.Add(dot)
127+
}
128+
return acc.GetElem(0) + acc.GetElem(1) + acc.GetElem(2) + acc.GetElem(3)
129+
}
130+
131+
// ロード済みのデータ(初期化は省略)
132+
var (
133+
testQuery8 []int8
134+
testQuery16 []int16
135+
testQuerySIMD [][8]int16
136+
testTarget16 []int16
137+
testTargetSIMD [][8]int16
138+
)
139+
140+
// --- ベンチマーク関数 ---
141+
142+
func BenchmarkPrimitive_RealData(b *testing.B) {
143+
b.ResetTimer()
144+
for i := 0; i < b.N; i++ {
145+
_ = dotProductPrimitive(testQuery16, testTarget16)
146+
}
147+
}
148+
149+
func BenchmarkSIMD_RealData(b *testing.B) {
150+
b.ResetTimer()
151+
for i := 0; i < b.N; i++ {
152+
_ = dotProductSIMD(testQuerySIMD, testTargetSIMD)
153+
}
154+
}
155+
```
156+
157+
# ベクトルの作成
158+
159+
ローカルでollamaを動かして、それにファイルを投げてベクトル化をします。なお、ベクトル化する場合はあまり長すぎるデータを与えてもコンテキストから溢れてエラーになってしまうため、あらかじめ原稿データを小さい単位の.rstファイルに分割しておきました。だいたい節タイトルごとに10行とか20行以下ぐらいに区切り、節タイトルがファイル名となるように事前処理しました。
160+
161+
積をSIMDでやるために計算はint16でやっていますが、データ自体は8ビットのベクトルとしていました。このあたり、どのあたりが良いのかはチューニングしていきたいですね。
162+
163+
```go
164+
package main
165+
166+
import (
167+
"context"
168+
"encoding/json"
169+
"fmt"
170+
"log"
171+
"os"
172+
"path/filepath"
173+
174+
"github.com/ollama/ollama/api"
175+
)
176+
177+
// 保存用のデータ構造
178+
// key: ファイル名, value: int8のベクトル
179+
type VectorStore map[string][]int8
180+
181+
func main() {
182+
if len(os.Args) < 2 {
183+
fmt.Println("使用法: go run main.go <directory_or_file>")
184+
return
185+
}
186+
187+
inputPath := os.Args[1]
188+
client, _ := api.ClientFromEnvironment()
189+
ctx := context.Background()
190+
model := "nomic-embed-text"
191+
store := make(VectorStore)
192+
files, _ := filepath.Glob(filepath.Join(inputPath, "*.rst"))
193+
194+
for _, file := range files {
195+
content, err := os.ReadFile(file)
196+
if err != nil {
197+
continue
198+
}
199+
200+
// 1. Ollamaから float32 ベクトルを取得
201+
req := &api.EmbedRequest{Model: model, Input: string(content)}
202+
resp, err := client.Embed(ctx, req)
203+
if err != nil || len(resp.Embeddings) == 0 {
204+
log.Printf("Skip %s: %v", file, err)
205+
continue
206+
}
207+
208+
// 2. float32 -> int8 量子化
209+
floatVec := resp.Embeddings[0]
210+
int8Vec := make([]int8, len(floatVec))
211+
for i, v := range floatVec {
212+
val := v * 127.0
213+
if val > 127 {
214+
val = 127
215+
}
216+
if val < -128 {
217+
val = -128
218+
}
219+
int8Vec[i] = int8(val)
220+
}
221+
222+
shortName := filepath.Base(file)
223+
store[shortName] = int8Vec
224+
fmt.Printf("Vectorized: %s\n", shortName)
225+
}
226+
227+
// 3. JSONファイルとして書き出し
228+
outputFile := "vectors.json"
229+
jsonData, _ := json.MarshalIndent(store, "", " ")
230+
os.WriteFile(outputFile, jsonData, 0644)
231+
232+
fmt.Printf("\nSuccess! Saved to %s\n", outputFile)
233+
}
234+
```
235+
236+
検索の方のプログラムは全文は載せませんが、すでに載せているロジックの組み合わせでいきます。検索用語の方も上記のベクトル化と同じようにベクトル化を行います。そして、[][8]int16にすべてベクトルを変換しておき、あとはドット積の大きい順序でソートして返すだけです。
237+
238+
# まとめ
239+
240+
AI界隈でよくでてくる計算がGoでも簡単に実現できるようになりました。類似検索とかベクトルデータベースの実装が今後増えることを期待しています。対応アーキテクチャも今後増えていくでしょう。WASMも対応になると嬉しいですね。
241+
242+
明日は武田さんになります。
29.8 KB
Loading
69.8 KB
Loading

0 commit comments

Comments
 (0)