24
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-10-05

表題の通り、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バイト以下の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以下になったわけで、私の用途には非常に良い結果が得られました。

  1. もともと3バイト整数をBase64でASCII4文字にしていた

24
12
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?