Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

アイドル時に実行されるGC処理のパフォーマンスが悪く、フリーズすることがある #608

Closed
todesking opened this issue Aug 12, 2014 · 43 comments

Comments

@todesking
Copy link

特定のメモリ確保パターンにおいて、メモリ解放処理(eval.c:garbage_collect())のパフォーマンスが劣化して長時間フリーズする問題があるようです。

小さな再現パターンをまだ発見できていませんが、Unite.vim で大量(数万件)のcandidateを表示させるのを何度か繰り返した後で10秒ほど放置することで再現します(放置時に裏でgarbage_collectが実行される)。

パッチは以下。

todesking/vim@eed1844

@koron
Copy link
Member

koron commented Aug 12, 2014

つまり後に作った dict から順番に開放されるようなケースを作ればいいんですかね?

@koron
Copy link
Member

koron commented Aug 12, 2014

今の構造なら、1000個も作れば O(n^2) で100万回オーダーでループが走りそう。

@Shougo
Copy link
Member

Shougo commented Aug 12, 2014

👍

お疲れ様です。
私もこの問題を調べていた時がありまして、大量にメモリを消費していると(Vim のメモリ消費量 700MB 程度で確認) 10 数秒 GC で Vim がフリーズします。

unite.vim 特有の問題ではないはずですが、unite.vim を使っている時に起きやすいです。
原理上は ctrlp.vim 等でも起きると思うのですが……。

@koron
Copy link
Member

koron commented Aug 12, 2014

@Shougo たとえば、ループ内でdict作って、そのメンバーとして前回のループで作ったdictを格納する、みたいなことしてますか?
コードで示すとこんな感じ

let d = {}
let i = 0
while i < 10000
  let d = { prev: d }
  let i += 1
endwhile

" use d

@Shougo
Copy link
Member

Shougo commented Aug 12, 2014

unite.vim は前回の candidatesをキャッシュしているので、似たようなコードになっているのではないかと考えています。パフォーマンスを稼ぐために候補のキャッシュが分散しており、どこに何が格納されているかは私でさえ把握できていません。

@todesking
Copy link
Author

まだ詳細を追っていないのですが、Vimのメモリ解放タイミングはいくつかあるようです。
例えばlet var = new_value 実行時には古い値の参照カウントを減少させて0なら開放する処理が走っている模様(set_var_lval()内でclear_tv()が呼ばれている)

それらのタイミングで開放できなかったメモリがgarbage_collect()で処理されているようですが、それがどういった条件なのかがわからず。おそらく循環参照まわりじゃないかと思いますが。

@h-east
Copy link
Member

h-east commented Aug 12, 2014

いちおうユーザが呼び出すことも出来るのね
:h garbagecollect()

@Shougo
Copy link
Member

Shougo commented Aug 12, 2014

はい。その通りです。VimのGCはPythonと同じで参照カウント + Mark Sweep になっています。
参照カウントでは循環参照が開放できないので、Mark Sweep を併用しています。
コードを見た限りだと、garbage_collect() を呼んだ時、メモリが足りない時(メモリ確保失敗)、
Vim がアイドル状態の時(CursorHold のタイミング)で Mark Sweep GC が呼ばれるようです。

unite.vim の場合、リストを大量に生成しており、それらの中身も非常に複雑となっています。
循環参照が発生しやすいので Mark Sweep GC が呼ばれるのではないかと考えられます。

@todesking
Copy link
Author

今試したら一瞬で再現出来ました(;´Д`)

let s:hoge = {}
let s:fuga = {}
let s:arr = []

function! Prepare() abort " {{{
  let s:hoge.a1 = map(range(1, 100000), '{}')
  let s:hoge.a2 = map(range(1, 100000), '{}')
  let s:hoge.a3 = map(range(1, 100000), '{}')
  let s:hoge.fuga = s:fuga
  let s:fuga.hoge = s:hoge
endfunction " }}}

function! Release() abort " {{{
  let s:arr = s:hoge.a2
  unlet s:hoge
  unlet s:fuga
endfunction " }}}

call Prepare() -> call Release() -> call garbagecollect() でフリーズ

@Shougo
Copy link
Member

Shougo commented Aug 12, 2014

おお、素晴らしい。

@todesking こちらでも .vimrc なし、上記の手順で再現しました。これはいけそうです。

@ynkdir
Copy link
Member

ynkdir commented Aug 13, 2014

list も同じ問題ありです

@todesking
Copy link
Author

パッチ(改)

diff --git a/src/eval.c b/src/eval.c
index ae8331d..8fd3d99 100644
--- a/src/eval.c
+++ b/src/eval.c
@@ -6900,7 +6900,9 @@ free_unref_items(copyID)
     int copyID;
 {
     dict_T     *dd;
+    dict_T     *dd_next;
     list_T     *ll;
+    list_T     *ll_next;
     int                did_free = FALSE;

     /*
@@ -6912,11 +6914,10 @@ free_unref_items(copyID)
            /* Free the Dictionary and ordinary items it contains, but don't
             * recurse into Lists and Dictionaries, they will be in the list
             * of dicts or list of lists. */
+           dd_next = dd->dv_used_next;
            dict_free(dd, FALSE);
            did_free = TRUE;
-
-           /* restart, next dict may also have been freed */
-           dd = first_dict;
+           dd = dd_next;
        }
        else
            dd = dd->dv_used_next;
@@ -6933,11 +6934,10 @@ free_unref_items(copyID)
            /* Free the List and ordinary items it contains, but don't recurse
             * into Lists and Dictionaries, they will be in the list of dicts
             * or list of lists. */
+           ll_next = ll->lv_used_next;
            list_free(ll, FALSE);
            did_free = TRUE;
-
-           /* restart, next list may also have been freed */
-           ll = first_list;
+           ll = ll_next;
        }
        else
            ll = ll->lv_used_next;

@todesking
Copy link
Author

"next dict/list may also have been freed" というコメントがどうにも不安なんですが、そういうケースないように見える……

@ynkdir
Copy link
Member

ynkdir commented Aug 13, 2014

ありがとうございます
dict_free, list_free に recurse が追加される前のコメントなので古いのかもしれません

@mattn
Copy link
Member

mattn commented Aug 18, 2014

お忙しそうな感じなので最新でパッチ作成

https://gist.github.com/0c58a7398c63ab4c3066

@mattn
Copy link
Member

mattn commented Aug 18, 2014

良さそうに思います。

@todesking パッチ提出お願い出来ますか?

@todesking
Copy link
Author

了解しました(vim_devにですよね?)

@mattn
Copy link
Member

mattn commented Aug 18, 2014

そのとおり!

@h-east
Copy link
Member

h-east commented Aug 18, 2014

「このscriptがxx秒掛かっていたけどこのpatchをあてたらyy秒に改善されたぜ!」みたいな数値的があればgood.

@mattn
Copy link
Member

mattn commented Aug 25, 2014

@todesking お忙しい様なら送信代行(お名前は存じております)出来ますがどうしましょうか?

@Shougo
Copy link
Member

Shougo commented Aug 25, 2014

「このscriptがxx秒掛かっていたけどこのpatchをあてたらyy秒に改善されたぜ!」みたいな数値的があればgood.

let s:hoge = {}
let s:fuga = {}
let s:arr = []

function! Prepare() abort " {{{
  let s:hoge.a1 = map(range(1, 100000), '{}')
  let s:hoge.a2 = map(range(1, 100000), '{}')
  let s:hoge.a3 = map(range(1, 100000), '{}')
  let s:hoge.fuga = s:fuga
  let s:fuga.hoge = s:hoge
endfunction " }}}

function! Release() abort " {{{
  let s:arr = s:hoge.a2
  unlet s:hoge
  unlet s:fuga
endfunction " }}}

let start = reltime()
call Prepare()
call Release()
call garbagecollect()
echomsg string(reltimestr(reltime(start)))

上記スクリプトで測定したかったのですが、私の環境ではパッチなし状態ではスクリプト実行後にフリーズするので測定が不能です。パッチを当てると0.2598sで処理が終わります。
こういう情報でもよいでしょうか。

@h-east
Copy link
Member

h-east commented Aug 25, 2014

@Shougo 👍
range(1, 100000)の数値を減らしてpatch前の時間も計測出来るようにしてpatch前後で処理時間比較を提示するとbetterだと思いました。(in my opinion)

@ujihisa
Copy link
Member

ujihisa commented Aug 25, 2014

👍

@Shougo
Copy link
Member

Shougo commented Aug 25, 2014

let s:hoge = {}
let s:fuga = {}
let s:arr = []

function! Prepare() abort " {{{
  let s:hoge.a1 = map(range(1, 50000), '{}')
  let s:hoge.a2 = map(range(1, 50000), '{}')
  let s:hoge.a3 = map(range(1, 50000), '{}')
  let s:hoge.fuga = s:fuga
  let s:fuga.hoge = s:hoge
endfunction " }}}

function! Release() abort " {{{
  let s:arr = s:hoge.a2
  unlet s:hoge
  unlet s:fuga
endfunction " }}}

let start = reltime()
call Prepare()
call Release()
call garbagecollect()
echomsg string(reltimestr(reltime(start)))

上記スクリプトで、
パッチなし: 100 秒
パッチあり: 0.133967 秒

備考:なぜか GC はスクリプトの実行後に発動するのでパッチ無し状態はストップウォッチにて測定しています。

@mattn
Copy link
Member

mattn commented Aug 25, 2014

👍

@todesking
Copy link
Author

お忙しい様なら送信代行(お名前は存じております)出来ますがどうしましょうか?

申し訳ございません、お願いします

@mattn
Copy link
Member

mattn commented Aug 26, 2014

申し訳ございません、お願いします

LGTM

@mattn
Copy link
Member

mattn commented Aug 26, 2014

@Shougo
Copy link
Member

Shougo commented Aug 26, 2014

👍

@mattn
Copy link
Member

mattn commented Sep 3, 2014

This looks too simple. There is a remark that the next dict may have
been freed, I think that was there for a good reason (dict containing a
dict). On the other hand, the comment just a few lines up suggests that
this is not possible. I wonder which one of the two comments is
outdated...

It's possible to pass dd_next to dict_free() (or use an ugly global
variable) and set it to NULL if it gets freed.

こう言われてますが、何か反論のある方はいますか?

@koron
Copy link
Member

koron commented Sep 3, 2014

現在の1つ解放するたびに先頭からやりなおすのは明らかにやり過ぎ。
かといえ指摘されているように、パッチのまったくやり直さないってのも問題。
たぶん辞書の作成順序と格納関係を逆にするとリークするか、
まったく速くならないことが考えられる。

解決策なんですが、こんな風に折衷案にするのはどうでしょう。

  1. 先頭から順番に、解放されるべきかチェックする。
  2. 解法されるべきオブジェクトが見つかったら、実際に解放し、「解放したフラグ」を立てる。
  3. 末尾までチェックし終わったら、「解放したフラグ」をチェックし、立っていなければGC終了。
  4. 立っていたらフラグを寝かせて 1 からやり直し。

@todesking
Copy link
Author

This looks too simple. There is a remark that the next dict may have been freed, I think that was there for a good reason (dict containing a dict). On the other hand, the comment just a few lines up suggests that this is not possible. I wonder which one of the two comments is outdated...

「コメントには"next dict may also have been freed"と書いてあるのでこのパッチだと不十分ではないか。しかしちょっと上のコメントには矛盾するようなこと(再帰的な開放は行わない)が書いてあってどっちが正しいんだろう。」という意見ですよね。

これはたぶん上のコメントが正しくて、現在はdictやlistの中身を再帰的に開放はしないのでこのままの処理でいいんじゃないかと思ってます。ソースを見る限りはそうなっているように見える。

@Shougo
Copy link
Member

Shougo commented Nov 26, 2014

このパッチですが、現在どういうステータスなのでしょうか。
vim_dev に投げられているものの、todo リストには入っておらず長期間放置されているように見えます。
パッチが既に完成しているのなら、todo に入っていない理由をvim_dev 上で尋ねてみる必要があると思います。

@Shougo
Copy link
Member

Shougo commented Nov 27, 2014

mattn さん対応有難うございます。

https://groups.google.com/d/msg/vim_dev/DBYOdHQWvqY/asNnl1fVBQgJ

@mattn
Copy link
Member

mattn commented Feb 3, 2015

@mattn mattn closed this as completed Feb 3, 2015
@ujihisa
Copy link
Member

ujihisa commented Feb 3, 2015

おおおおおおおおおおおおおおおおおおおおおおおおおおおおお

@rhysd
Copy link

rhysd commented Feb 3, 2015

🎉

@Shougo
Copy link
Member

Shougo commented Feb 3, 2015

👍

@crazymaster
Copy link
Member

👏

@todesking
Copy link
Author

皆さんありがとうございました!!!

@koron
Copy link
Member

koron commented Feb 4, 2015

👍
2015/02/04 13:58 "todesking" notifications@github.com:

皆さんありがとうございました!!!


Reply to this email directly or view it on GitHub
#608 (comment).

@ujihisa
Copy link
Member

ujihisa commented Feb 4, 2015

感極まってきました

@todashuta
Copy link
Member

㊗️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants