MasaのITC Life

夢は起業家!全てにおいて凡人だけど頑張ることだけはいっちょ前!

プログラミング

【メモリ効率化】Go言語でmake()を使うときの注意点!

投稿日:2020年5月28日 更新日:



今回はGo言語とメモリのお話です。

make()をやみくもに使うと、

メモリ領域を無駄に消費します。



しかしちょっと気を付けるだけで、

メモリ使用率を大きく減らせます。

当実験コードではメモリ効率が

約40%も向上しました!



make()の動作を分かっていないと、

何が起きているのかわからないので、

最初のmakeとメモリについて

一度確認していきましょう!




メモリで見るGo言語のmake関数



makeは変数の宣言に使われます。

スライスを作る際は、

makeの動作に注意を払う必要があります。



make自体が悪いのではなく、

makeを使用した後の、

プログラマーによるケアが大事なのです。



スライスは配列へのポインタ等を

構造体にまとめたものです。

つまりスライスは

配列を参照するわけです。



ここで例としてソースを見てみましょう。


スライスを定義して、

makeで初期化を行っているだけです。



スライス自身と参照先アドレスを

標準出力します。その後もう一度、

配列の箱を作り直します。



上のソースを実行すると、

以下の実行結果になります。




スライス自体のアドレスは変わっていません。

しかしmake()をし直すたびに、

参照先である配列のアドレスが変わります。

make()は動的にメモリを確保します。




ここで疑問になるのは、

make()前の配列はどこにいくのか・・・

結論を先に申しますと、

その配列オブジェクトに

誰もアクセス出来なくなるので、

ゴミとしてヒープ領域に残ります。



とは言ってもGo言語には、

ガベージコレクタ
が標準で

実装されているので、

どこかのタイミングで、

そのゴミを回収しに来ます。



ゴミとなった配列は役割がないので、

メモリを無駄に消費するだけです。

回収されたごみは、OSに返上されるか、

そのプロセスによって再利用されます。



以上が今回のキーポイントです。



それでは実際にmakeを連発したときの

メモリの使用状況を見てみましょう。





Go言語のガベージコレクタとメモリ使用効率



まずはimportパッケージです。




ついでに収集したデータは、

csv形式で保存するので、

そのための関数も載せておきました。




長さ20の多重スライスを用意して、

内部スライスをmake()で初期化しています。

その際にインデックスと長さは

乱数でそれぞれ指定します。



for分で無限ループしており、

1週する度にmake関数を呼びます。

bytes変数はpool多重スライスが参照する

配列群の合計メモリ容量(バイト)です。



またforが1週する度に、

メモリ(ヒープ領域)の状況も取得します。

取得したデータは、csvで保存しつつ、

標準出力も行っています。






これを実行すると以下の結果になります。

以下は取得したメモリ要素の

一部をグラフ化したものです。

詳しい情報はこちら(英語)



pool多重スライスが参照する配列群は、

make()で初期化されるにつれて増大し、

約15Mバイトのところで頭打ちになります。



HeapSys プロセスがOSから割り当てられたメモリ容量
HeapAlloc ヒープ領域中のオブジェクトに割り当てている容量
HeapIdle ヒープ領域中の使用されていない容量
HeapReleased 解放されたヒープ領域の容量



ここで問題なのは、

pool多重スライス自他は

約15Mバイト程度なのに、

OSからはその2.5倍である

約37Mバイトが割り当てられています。



ヒープ領域はスタック領域と違い、

動的に確保されたものが格納されるので、

今回で言うならばpool多重スライスが

そのほとんどを占めているはずです。



実際はmake()によって、

再初期化した際に出た旧配列が

ゴミとなってヒープに溜まっています。



ガベージコレクションによって、

定期的にHeapIdleの再利用が

されるはずですが不完全です。



それゆえにのこぎり波となり、

ヒープ領域に少しゴミとして

取り残されています。



それではどのように

解決したら良いのでしょうか。




火種はmakeで初期化された際の、

旧配列にあるわけですから、

これを再割り当てできる機構を

手動で作ってあげるわけです。



と言っても少しコードを

変えてあげるだけで解決します。

beforeに該当するところを

afterに書き換えるだけです。

骨組みは同じです。



大きさ5のバッファーを用意します。

バッファーに旧配列のアドレスが

溜まっていればそれを

makeする代わりに割り当てます。



溜まってなければ、

先ほど同様にmake()で割り当てます。



参照する配列を上書きする時点で、

前の配列への参照を失うところを、

今回はバッファーへ保存するわけです。




実行結果はこのようになります。




HeapIdleは、HeapReleasedと

被っていて見にくいですが、

ほとんど再利用できています。


pool多重スライスが約15Mバイトと、

先ほどの結果と変わっていないのに、

ヒープの全体容量(HeapSys)は、

37MBから16MBまで少なくなりました。






これがメモリ使用効率向上の仕組みです。

最後まで読んでいただきありがとうございました。



参考:
https://blog.cloudflare.com/recycling-memory-buffers-in-go/



-プログラミング

Copyright© MasaのITC Life , 2023 All Rights Reserved Powered by STINGER.