表題の通り、Goのstring型を何の気なしに使っていたら思ったよりバイナリサイズ・消費メモリサイズが大きくなっていたという話題です。
Goプログラミングでバイナリサイズやメモリ消費量をカリカリにチューニングすることは多くないと思われるので、本稿の内容が重要になることは滅多にないだろうことを最初にお断りしておきます。
mapのサイズが案外大きい
ある日、筆者は次のようなGoコードを書いていました。
var oui = map[string]string{
"AAAB": "Xerox",
"AAAC": "Xerox",
"AAAD": "Xerox",
"AAAE": "Xerox",
"AAAF": "Xerox",
/* snip */
"/Pv7": "Cisco",
"/PxI": "Apple",
"/P53": "HitachiR",
"/P7C": "Invensys",
"/P+q": "IeeeRegi",
}
このmapは要素数が約2.4万、キーはASCII4文字固定、値はASCII2〜8文字というものです。これをビルドしてみると、このmapがバイナリサイズのうち950KBほどを占めていることに気づきました。
1要素あたりASCII6文字〜12文字しか情報が無いはずなのに、950KB/2.4万=約40バイトということで案外サイズが大きいと感じます。
mapのキーの型をstring
からint32
に変えてみた
思ったよりサイズを食っていると感じたので、mapのキーの型をstring
からuint32
に変更してみました。キーはASCII4文字固定1なので1:1対応で32bit整数に変換できます。
var oui = map[uint32]string{
0x1: "Xerox",
0x2: "Xerox",
0x3: "Xerox",
0x4: "Xerox",
0x5: "Xerox",
/* snip */
0xfcfbfb: "Cisco",
0xfcfc48: "Apple",
0xfcfe77: "HitachiR",
0xfcfec2: "Invensys",
0xfcffaa: "IeeeRegi",
}
このように変更したところ、バイナリサイズが380KBほど減少しました。計算してみると、1要素あたり16バイトほど減った計算になります。
いくらなんでも減りすぎではないか?と感じたので原因を調べてみました。
string
型はどう実現されているか
今回の現象の原因はstring
型の実装にあります。string
型に対応する構造体は次のようなものです。
type stringStruct struct {
str unsafe.Pointer
len int
}
( src/runtime/string.go より)
つまり、string
型は文字列実体へのポインタと文字列長を表すint
型とで構成されています。64bit環境であればポインタで8バイト、文字列長で8バイト、さらに文字列の実体の分もディスクまたはメモリが必要になります。mapのキーをstring
型(8+8+4文字=20バイト)からint32
(=4バイト)に変更することで1要素あたり16バイト節約できたのは当然ということになります。
8バイト以下のstring
をuint64
にする
同じノリでmapの値の型もuint64
にしてみました。
var oui = map[uint32]uint64{
0x1: 0x0000005865726f78, // Xerox
0x2: 0x0000005865726f78, // Xerox
0x3: 0x0000005865726f78, // Xerox
0x4: 0x0000005865726f78, // Xerox
0x5: 0x0000005865726f78, // Xerox
/* snip */
0xfcfbfb: 0x000000436973636f, // Cisco
0xfcfc48: 0x0000004170706c65, // Apple
0xfcfe77: 0x4869746163686952, // HitachiR
0xfcfec2: 0x496e76656e737973, // Invensys
0xfcffaa: 0x4965656552656769, // IeeeRegi
}
容量削減のために無理をしている分、値を取り出すのに多少頑張る必要があります。
abbr64, ok := oui[oui24hash]
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, abbr64)
abbr := string(bytes.TrimLeft(b, "\x00"))
これをビルドすると更に290KB減となり、期待通りバイナリサイズ削減効果が得られました(同じ値を持つ要素があるため、キーのときより削減幅が小さくなっています)。当初のサイズと比較すると約950KBから約280KBと1/3以下になったわけで、私の用途には非常に良い結果が得られました。
-
もともと3バイト整数をBase64でASCII4文字にしていた ↩