Goのstring型が思ったより容量食いだった話

  • 14
    いいね
  • 0
    コメント

表題の通り、Goのstring型を何の気なしに使っていたら思ったよりバイナリサイズ・消費メモリサイズが大きくなっていたという話題です。

Goプログラミングでバイナリサイズやメモリ消費量をカリカリにチューニングすることは多くないと思われるので、本稿の内容が重要になることは滅多にないだろうことを最初にお断りしておきます。

mapのキーの型をstringからint32に変えてみた

ある日、筆者は次のような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ほどを占めていることに気づきました。

思ったよりサイズを食っていると感じたので、mapのキーの型をstringからuint32に変更してみました。

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バイト以下のstringuint64にする

同じノリで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以下になったわけで、私の用途には非常に良い結果が得られました。