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

メモリプールを使う事でメモリ確保と解放を高速化 #985

Merged
merged 7 commits into from
Aug 15, 2019
Merged

メモリプールを使う事でメモリ確保と解放を高速化 #985

merged 7 commits into from
Aug 15, 2019

Conversation

beru
Copy link
Contributor

@beru beru commented Aug 10, 2019

PR の目的

ログファイル等の行数が多いファイルを読んで表示するのに掛かる時間を減らす。

カテゴリ

  • 速度向上

PR のメリット

行数が極端に多いファイルを開く時の待ち時間が短くなります。またメモリ使用量も少し減ります。あと分かりづらいですがそのファイルを閉じる時の時間も短くなります。

テストケースとしてちょっと極端な例ですが、a\r\n という行が 16777216行連続しているファイルを開いてから編集出来るようになるまでの待ち時間が短くなるのが体感できます。

そのファイルをzipで圧縮したもの → test.zip

自分の環境では以前は 7.8 秒ぐらい掛かっていたのが 6.7 秒ぐらいになりました。Visual Studio 2017 の Performance Profiler で起動して上記の大きいファイルを開いて確認しました。

PR のデメリット (トレードオフとかあれば)

以前は極端に行数が多い状態を作った後に行数が短い状態を作るとメモリ解放が行われてメモリ使用量が減りましたが、このメモリプールを使うやり方にすると減りません。

メモリ解放のタイミングが CDocLineMgrCLayoutMgr が解放されるタイミングになるので、エディタのプロセスを閉じるまでメモリプールのメモリ解放がされません。

実際のユースケース的にそれで困るという事はあんまり考えられない気もします。

PR の影響範囲

CDocLineMgrCLayoutMgr

関連チケット

#312 #980

@beru
Copy link
Contributor Author

beru commented Aug 10, 2019

C++17 以降では std::pmr::unsynchronized_pool_resource というのが有るのでこれを使えば自前で実装したクラスを使わないでも目的が達成出来るかと思ったのですが、メモリ使用量がやけに増加してしまうので、やはり自前クラスを使う事にしました。std::pmr::pool_options の値を調整してもメモリ使用量を減らす事が出来ませんでした。標準ライブラリ側の実装の問題でしょうか?将来的には解消されると良いですが…。

単体テストについては #980 ではリクエストが有ったので追加しましたが、結局のところ役に立っていないと判断してこのPRでは追加していません。レビューアーの方々の考えとして、「単体テストが実装されていないコードなんてちゃんと動く保証が無いので Approve は出来ない(むしろ Reject したい)、動作確認するのは面倒なのでやりたくない」という事であれば、なんかもう代わりに改良してくれよ、って感じです。

@beru beru added the 🚅 speed up 🚀 高速化 label Aug 10, 2019
@AppVeyorBot
Copy link

Build sakura 1.0.2114 completed (commit f0f6203e88 by @)

@berryzplus
Copy link
Contributor

リトライありがとうございます:smile:
まず、前回より修正対象が増えてるようなので一時判断まで1~2日欲しいです。

C++17 以降では std::pmr::unsynchronized_pool_resource というのが有るのでこれを使えば自前で> 実装したクラスを使わないでも目的が達成出来るかと思ったのですが、メモリ使用量がやけに増加してしまうので、やはり自前クラスを使う事にしました。

c++の準拠規格が17に変更されていて、CLayoutMgrでstd::pmr::unsynchronized_pool_resourceが使われているように見えました。使うの使わないのどっち?な感じなので、可能なら確認しといて欲しいです。もちろんぼくもあとで見とこうと思っていますが :smile:

未定義の場合の準拠規格はc++14相当なので、準拠規格の変更にあたると思います。何が違うのかメリデメを説明するのがめんどくさいので、これに関しては「ついでに入れちゃえ!」でもいいです。後追いでMinGW側のメイクファイルとかCMakelists.txtにも横展開することになるのでいずれにしろ変更レビューが入りますし。

@beru
Copy link
Contributor Author

beru commented Aug 10, 2019

リトライありがとうございます😄
まず、前回より修正対象が増えてるようなので一時判断まで1~2日欲しいです。

同じ内容の PR を何度も作成して手間をかけてしまいすみません。

レビューアーの方々、特に @m-tmatma さんの労力を無駄にしてしまい本当に申し訳ないと思います。指摘については出来るだけ対応しますが、小改良に関して綿密にコメントをやり取りするのはコスト掛け過ぎな気もするのでもうちょっとレビューの基準を緩めてほしいです。

C++17 以降では std::pmr::unsynchronized_pool_resource というのが有るのでこれを使えば自前で> 実装したクラスを使わないでも目的が達成出来るかと思ったのですが、メモリ使用量がやけに増加してしまうので、やはり自前クラスを使う事にしました。

c++の準拠規格が17に変更されていて、CLayoutMgrでstd::pmr::unsynchronized_pool_resourceが使われているように見えました。使うの使わないのどっち?な感じなので、可能なら確認しといて欲しいです。もちろんぼくもあとで見とこうと思っていますが 😄

最新のコミットでは、std::pmr::unsynchronized_pool_resource についてはコメントアウトして使っていない筈です。

未定義の場合の準拠規格はc++14相当なので、準拠規格の変更にあたると思います。何が違うのかメリデメを説明するのがめんどくさいので、これに関しては「ついでに入れちゃえ!」でもいいです。後追いでMinGW側のメイクファイルとかCMakelists.txtにも横展開することになるのでいずれにしろ変更レビューが入りますし。

致命的な問題が無ければ C++17 に変更してしまって良いと思います。MinGW側の対応は後で別のPRにさせてもらえると楽で助かります。

Copy link
Contributor

@berryzplus berryzplus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

先に c++17 の導入提案をしましょう 😄

この PR で提案されているメモリ管理の手法は c++17 の導入された考え方を前提にしています。既に、azure pipelinesのMinGWビルドがコケていて(=悪い影響がでている)、あまり良くない感じになっています。導入提案を単発で行うこと自体にもメリットがあると思うので、先に済ませておきたいです。

導入提案で「c++17でできること」を共有しておけば、メソッド引数の属性マーキングに「これはなんですか?mdに記載してください。」みたいなアフォな指摘が付く事態も防げる気がします。(実際にあるかどうかは別にして容易に想定できる事態です 😭

現状で細かい指摘を入れてもMinGWビルドがコケて微妙な感じになるのは明白なので、導入提案が通ってrebaseができるまで指摘は保留します。(しょーもない指摘は先行で書いてしまうかも知れません。

c++導入提案のPRですが、可能ならお願いしたいです。

出なかったとしてもc++17導入のハラは決まったので、盆休み中にはPRを出したい感じです。
たぶん、メリデメ書くには詳しい人のほうがいいと思うので。
(ぼくがレビュアーに回ると「評価がザル」っていうメリットもあるよ!...orz

@beru
Copy link
Contributor Author

beru commented Aug 11, 2019

先に c++17 の導入提案をしましょう 😄

この PR で提案されているメモリ管理の手法は c++17 の導入された考え方を前提にしています。既に、azure pipelinesのMinGWビルドがコケていて(=悪い影響がでている)、あまり良くない感じになっています。導入提案を単発で行うこと自体にもメリットがあると思うので、先に済ませておきたいです。

PRを分ける事で実質的に何が得られるのか自分には良く分かりませんが分けておきました。#986

導入提案で「c++17でできること」を共有しておけば、メソッド引数の属性マーキングに「これはなんですか?mdに記載してください。」みたいなアフォな指摘が付く事態も防げる気がします。(実際にあるかどうかは別にして容易に想定できる事態です 😭

そもそもなんで導入提案で「c++17でできること」の解説をわざわざしないといけないんでしょうか?興味がある人なら自分で調べるだろうし、興味は無いけど良く分からないから反対だけするという人がいれば意味もなく保守的だなぁ…って思います。

ターゲットがPCのアプリの開発で開発環境で新しい言語規格を使えるなら気兼ねなく使っても問題無いと思うんですが…。業務の都合上、古いバージョンのVisualStudioを使う必要があるわけではないし。

今現在サクラエディタの開発をしてる人でこれこれこういう理由で使いたくないんだ、っていうのがあれば聞いてみたいところです。

@berryzplus
Copy link
Contributor

そもそもなんで導入提案で「c++17でできること」の解説をわざわざしないといけないんでしょうか?

デメリットがある(=既存コードを書き換えないといけない)からです。

PRを分ける事で実質的に何が得られるのか自分には良く分かりませんが分けておきました。#986

@beru さんのレベルなら「たくさんの変更」を1つのPRに含めても、どのファイルのどの修正が何を変更するものなのかを適切に判断できると思います。PRを分けることのメリットは、ある目的を果たすための変更が正しく行われているか否かの判断を容易に行えるようになることです。レビュー参加にあたって求められるレベルを下げることができます。2つ以上の目的を含むPRは、全部の目的が承認(or 容認)されないとマージできませんが、目的が1つならば1つの容認を取り付ければマージできます。目的を1つにすることにより、話の争点がブレにくくなると思います。

@beru
Copy link
Contributor Author

beru commented Aug 11, 2019

そもそもなんで導入提案で「c++17でできること」の解説をわざわざしないといけないんでしょうか?

デメリットがある(=既存コードを書き換えないといけない)からです。

既存コードの書き替えについては VC++ だと書き換えなくてもビルドが通りましたが、MinGW だとコンパイルエラーが生じていました。sakura_core/macro/CWSH.cpp の記述を直すだけでビルドが通る事は確認出来ました。その事が誰もが移行に躊躇するほどのデメリットなのかというと、そんな事無いんではと思うんですが…。自分が把握していないだけで実はもっと色々と変更が必要なんでしょうか??

あと「c++17でできること」って色々あり過ぎませんか?調べればいくらでも詳しい情報が見つかるのに、PRの導入文で何でそんな事を書かなきゃいけないのかもうわけがわかりません。。まぁ話の流れの中で適当に書いた事なのかもしれませんが…。。

@beru さんのレベルなら「たくさんの変更」を1つのPRに含めても、どのファイルのどの修正が何を変更するものなのかを適切に判断できると思います。PRを分けることのメリットは、ある目的を果たすための変更が正しく行われているか否かの判断を容易に行えるようになることです。レビュー参加にあたって求められるレベルを下げることができます。2つ以上の目的を含むPRは、全部の目的が承認(or 容認)されないとマージできませんが、目的が1つならば1つの容認を取り付ければマージできます。目的を1つにすることにより、話の争点がブレにくくなると思います。

なぜ別けた方が良いのかについての丁寧な解説ありがとうございます。別けた方が明確になるのはその通りだと思います。でも別けないと何やってるのか分からないくらい複雑なPRでも無いと思いますし、細かい事はどうでもいいじゃないかって思うんですけど…。

@beru
Copy link
Contributor Author

beru commented Aug 12, 2019

Azure Pipelines で mingw のビルドが失敗しています

internal error in mingw32_gt_pch_use_address, at config/i386/host-mingw32.c:186: MapViewOfFileEx: Attempt to access invalid address. 

precompiled header のファイルをビルド前に消さないといけないとか書いてるページがありました。

http://dev-list.musescore.org/Windows-compilation-error-td7577671.html

そもそもなぜ mingw のビルドまで抱えないといけないのだろう… zzz

@AppVeyorBot
Copy link

Build sakura 1.0.2127 completed (commit e8b5621d50 by @)

@berryzplus
Copy link
Contributor

/AzurePipelines run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@berryzplus
Copy link
Contributor

そもそもなぜ mingw のビルドまで抱えないといけないのだろう… zzz

MinGWビルドの結果は一旦スルーでいいです。
azure pipelinesの仕様がこちらの要求とマッチしてないっぽい。
(=再実行がクリーンビルドになってない。まぁ、そっちがスタンダードだろうけど 😄 )

@takke
Copy link
Member

takke commented Aug 12, 2019

MinGW もうなくせばいいのにーとか思うんですけどここに書く話じゃないですね。

@beru
Copy link
Contributor Author

beru commented Aug 13, 2019

README.md には MinGW の対応必須ですよとは書かれてませんが対応したい人の判断では他の人のPRでもちゃんと対応せにゃあかん的な事なのかと…

きっと昔はついでに見ておくか的なものだったんでしょうけど…

Copy link
Contributor

@berryzplus berryzplus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

一時見解のコメント付けま~す。

MinGWビルドを維持するのは、ほぼ「ぼくの趣味」です。
あれのルーツは、「サクラエディタをMinGWでビルドしてみた(ひつまぶし@名古屋)」です。
MSVCでしかビルドできない変なコードにはしたくないから、「別なコンパイラでもビルドしてチェックをかけたい」が現状の存在理由で、コンパイラがgccである必然はありません。

明らかにコードが原因でない問題の発生時は「一旦放置」で良いと考えています。
維持の責任をPR作成者に任せるつもりはありません。

: m_getIndentOffset( &CLayoutMgr::getIndentOffset_Normal ) // Oct. 1, 2002 genta // Nov. 16, 2002 メンバー関数ポインタにはクラス名が必要
: m_getIndentOffset( &CLayoutMgr::getIndentOffset_Normal ), // Oct. 1, 2002 genta // Nov. 16, 2002 メンバー関数ポインタにはクラス名が必要
m_layoutMemRes(new CPoolResource<CLayout>())
//m_layoutMemRes(new std::pmr::unsynchronized_pool_resource()) // メモリ使用量が大きい為に使用しないで、CPoolResource を代わりに使う
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

追加のメンバ初期化宣言を「, m_layoutMemRes」のように書けば、今後メンバ初期化を追加するときに迷う要素を減らせる気がします。よくよく考えてみたら、それほどしょーもない指摘でもなかった・・・。

例:

CLayoutMgr::CLayoutMgr()
    : m_getIndentOffset( &CLayoutMgr::getIndentOffset_Normal )	//	Oct. 1, 2002 genta	//	Nov. 16, 2002 メンバー関数ポインタにはクラス名が必要
    , m_layoutMemRes(new CPoolResource<CLayout>())

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

記述スタイルは癖とか好みの問題なんで自分は周りに合わせた方が良いと感じた時は合わせてました。

今回は berryzplus さんの指摘に従う事にします。

@@ -95,7 +98,8 @@ void CLayoutMgr::_Empty()
CLayout* pLayout = m_pLayoutTop;
while( pLayout ){
CLayout* pLayoutNext = pLayout->GetNextLayout();
delete pLayout;
pLayout->~CLayout();
m_layoutMemRes->deallocate(pLayout, sizeof(CLayout), alignof(CLayout));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m_layoutMemRes が指すポインタ型で、配置delを呼んでメモリ解放を行うメソッドを提供すれば1行で書けると思います。おそらく、1行で書けるメソッドを新設するほうがコードの意図が分かりやすくなる気がします。

Copy link
Contributor Author

@beru beru Aug 13, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m_layoutMemRes の型は std::unique_ptr<std::pmr::memory_resource> です。std::pmr::memory_resource にはそういうメソッドが無いのでそれを実現するのは厳しくないでしょうか?
https://en.cppreference.com/w/cpp/memory/memory_resource

std::pmr::polymorphic_allocator には construct メソッドがありますが、destroy メソッドはVS2017だと使えませんでした(追記 VS2019 16.3.0 Preview 1.0 でも無い事確認)。new_objectdelete_object メソッドは C++20 からのようです。当初のコミットでは std::pmr::polymorphic_allocator 経由のコードでしたが、メンバーの数が増えちゃうので使わないようにしました。

ちなみに何故抽象I/F型の std::pmr::memory_resource を使っているかというと差し替えられるようにする為にです。直接独自クラスを使うようにすれば berryzplus さんが提案するメソッドも追加して利用出来るし仮想関数呼び出しのオーバーヘッドも無いのでそうしたい気持ちも少し有ります。本来は(色々とレビューがややこしい事になりがちな)自作クラスを使わずに std::pmr::unsynchronized_pool_resource で済ませたかったんですがメモリ使用量が激増してしまったので諦めました。

Copy link
Contributor

@berryzplus berryzplus Aug 13, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

テキトーに行間読んでくださいね。こういうことです。
こういうことです。コードの行間はテキトーに補ってください。

template<typename T>
struct IMemoryPool
{
    virtual T* allocate() = 0;
    virtual void deallocate(T* p) = 0;
}

template<typename T>
class CPoolResource : public IMemoryPool<T>, public std::pmr::memory_resource
{
}

class CLayoutMgr
{
    std::unique_ptr<IMemoryPool> m_layoutMemRes;
}

CLayoutMgr::CLayoutMgr()
    : m_layoutMemRes(new CPoolResource<CLayout>()) {}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

いやーそこまで行間読むことは出来ませんでした。

その書き方にしたら将来的に std::pmr::unsynchronized_pool_resource への差し替えが行いにくくなるしどうかなぁと思います。それにそう書くくらいなら CPoolResource 直接使った方が良いのでは?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

テキトーに行間読んでくださいね。こういうことです。

いやーそこまで行間読むことは出来ませんでした。

ニュアンスが変に伝わったように感じたので訂正。

誤)テキトーに行間読んでくださいね。こういうことです。
正)こういうことです。コードの行間はテキトーに補ってください。

前者の書きっぷりだと「ちゃんと行間読み取ってもらわないと困ります。」の意味に取れないこともない気がしました。ぼくそんな高慢な発想しないんですが、そう読めないこともない 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#980 では CMemPool::Construct メソッド(Variadic template 使用)がメモリ確保とコンストラクタ呼び出しをまとめて行うようになっていて、CMemPool::Destruct はデストラクタ呼び出しとメモリ解放をまとめて行うようになってました。このPRでは抽象I/F型の std::pmr::memory_resource ベース前提で利用側の手続きが増えちゃってるので詳細を隠蔽したいマインドと相反するとは思います。

C++20 まで待てば std::pmr::polymorphic_allocatornew_objectdelete_object メソッドが追加されるみたいで型安全志向の人もにんまり、となるかもしれませんがなんちゃって銀の弾を使っているだけだと狼人間に食べられてしまいそうです。

https://tabesugi.net/memo/2009/1a.html

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

テキトーに行間読んでくださいね。こういうことです。

いやーそこまで行間読むことは出来ませんでした。

ニュアンスが変に伝わったように感じたので訂正。

誤)テキトーに行間読んでくださいね。こういうことです。
正)こういうことです。コードの行間はテキトーに補ってください。

前者の書きっぷりだと「ちゃんと行間読み取ってもらわないと困ります。」の意味に取れないこともない気がしました。ぼくそんな高慢な発想しないんですが、そう読めないこともない 😄

冷静に考えてみると、自然言語の数行の行間から20行程度の疑似コードを読み取ってくれ、っていうのも発想として無理がありますね(コンテキストから補間してそれが可能なケースもありそうですけど圧縮率がやけに高い…)

std::pmr::memory_resource ベースにしたせいで色々弊害というか記述がすっきりしない点が出てますね。#980 では cacay/MemoryPool を参考にした自作メモリプールだけで済ませたらその実装内容の詳細問い詰めコースに入ってしまいましたが、このPRでは今後どうなる事やら ❔

@@ -381,7 +385,7 @@ CLayout* CLayoutMgr::CreateLayout(
CLayoutColorInfo* colorInfo
)
{
CLayout* pLayout = new CLayout(
CLayout* pLayout = new (m_layoutMemRes->allocate(sizeof(CLayout))) CLayout(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m_layoutMemRes が指すポインタ型で、メモリ確保を行って配置newを呼び出すメソッドを提供すれば1行で書けると思います。おそらく、1行で書けるメソッドを新設するほうがコードの意図が分かりやすくなる気がします。

Copy link
Contributor Author

@beru beru Aug 13, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#985 (comment) で書いたのと同じ理由ですが、抽象I/F型である std::pmr::memory_resource にはそういうメソッドは無いので難しいと思います。std::pmr::polymorphic_allocator::new_object がそういうメソッドですが C++20 以降のもので使えません。

自分が加えた変更が少し読みにくいのは否定出来ませんが我慢してほしいです。

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

この指摘、対応不要っす。
同じ系統ぜんぶ(=xxx が指すポインタ型でry)。

@@ -589,7 +593,8 @@ CLayout* CLayoutMgr::DeleteLayoutAsLogical(
DEBUG_TRACE( _T("バグバグ\n") );
}

delete pLayout;
pLayout->~CLayout();
m_layoutMemRes->deallocate(pLayout, sizeof(CLayout), alignof(CLayout));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m_layoutMemRes が指すポインタ型で(ry

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#985 (comment) で回答しました。

@@ -37,7 +38,6 @@
#include "util/design_template.h"

class CBregexp;// 2002/2/10 aroka
class CLayout;// 2002/2/10 aroka
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

何故削る?削らなくても害がなさそうなのに削っているのが気になりました。

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

削らないでも害は確かに無いのかもしれませんが削ってもビルドが通るという事は不要な記述だろうと判断しました。

#include "mem/CMemoryIterator.h"

があってそこで

#include "doc/layout/CLayout.h"

しているので前方参照が不要という認識です。

「心のオアシスなんだから削るな!」と主張される場合は戻しておきます。

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

削る必要がないのであれば残しておいてもいい気がします。
論点がブレる要因になりうるので 😄

「削るべきじゃない」とまでは思わないので、他の気になるところが片付いたらスルーする予定です。

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

当初はヘッダファイル CLayoutMgr.hstd::pmr::polymorphic_allocator<CLayout> 型のメンバーを追加していて、その為に型のレイアウトをヘッダ側で知る必要があって前方宣言じゃ駄目なので削ったりしました。でも削った後に #include "doc/layout/CLayout.h" を追加しないでもビルドが通るので不思議に思って調べてみたら #include している別のヘッダファイルが #include してました。まぁつまり全て void* だけで頑張るべきという事でしょうか…。

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

単純に削ると CLayout には依存しなくなった の意図に見えます。

実態は 前方宣言じゃ不十分なくらい強く依存している で、前方宣言の代わりに #include "CLayout.h" を追加する必要があった だと思います。

削る必要はないけど削る、を是とするなら、
#include "CLayout.h" もできれば入れたいです。

前方宣言じゃ不十分で、includeしないといけない、が削った理由なのだから。

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

でも他のヘッダファイルが既に #include しているわけで、ポーズにしかならない気がします。

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

b67852b で前方宣言は削らないで元に戻しておきました。ここに関してはこだわる必要は無いですね。

:
m_docLineMemRes(new CPoolResource<CDocLine>())
//m_docLineMemRes(new std::pmr::unsynchronized_pool_resource()) // メモリ使用量が大きい為に使用しないで、CPoolResource を代わりに使う

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

同じことなんで、例示だけ書きます。

CDocLineMgr::CDocLineMgr()
	: m_docLineMemRes(new CPoolResource<CDocLine>())
{

中括弧の前の空行は要らない気がします。

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

独立した行に配置する事で CPoolResourcestd::pmr::unsynchronized_pool_resource のどちらかをコメントアウトさせての切り替えを行いやすくするためにそう書きました。まぁどうせ切り替えやすくする事が目的なら下記のように書いた方が良いかも知れないですが。

CDocLineMgr::CDocLineMgr()
	:
#if 1
	m_docLineMemRes(new CPoolResource<CDocLine>())
#else
	m_docLineMemRes(new std::pmr::unsynchronized_pool_resource()) // メモリ使用量が大きい為に使用しないで、CPoolResource を代わりに使う
#endif

今回は指摘に従って下のように書こうかと思います。

CDocLineMgr::CDocLineMgr()
	: m_docLineMemRes(new CPoolResource<CDocLine>())
//	: m_docLineMemRes(new std::pmr::unsynchronized_pool_resource()) // メモリ使用量が大きい為に使用しないで、CPoolResource を代わりに使う

@@ -68,15 +73,15 @@ CDocLineMgr::~CDocLineMgr()
//! pPosの直前に新しい行を挿入
CDocLine* CDocLineMgr::InsertNewLine(CDocLine* pPos)
{
CDocLine* pcDocLineNew = new CDocLine;
CDocLine* pcDocLineNew = new (m_docLineMemRes->allocate(sizeof(CDocLine))) CDocLine();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

前の指摘と同じです。 m_docLineMemRes に専用のメソッドを作って「 m_docLineMemRes で allocate する」とはどういうことか?の詳細を実装したメソッドを用意すべきだと思います。

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#985 (comment) と同じ回答になりますが、m_docLineMemRes の型は std::unique_ptr<std::pmr::memory_resource> です。std::pmr::memory_resource は標準ライブラリの抽象I/F型で、派生クラスを直接使用しているわけではないので指摘に対応するのが難しいのではないかと考えています。

#include <memory_resource>
#include <Windows.h>

// std::pmr::unsynchronized_pool_resource だとメモリ使用量が大きい為、自前実装を用意
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

正確な数字は要らんですが、どれくらい大きいかの情報があると嬉しいです。
たとえば「128MiB程度なら誤差なのでTS使ったほうがいいと思います」な考え方もありだと思います。

Copy link
Contributor Author

@beru beru Aug 13, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

x64 Release の場合で、a\r\n というだけの行が 16777216 行連続するファイルを開いた場合のメモリ使用量(タスクマネージャーで確認)を記載します。

方法 メモリ使用量(単位 MB)
new 2578.5
CPoolResource 2195.9
std::pmr::unsynchronized_pool_resource 3335.2

#985 (comment) にも記載しましたが、std::pmr::pool_options の値を調整しても std::pmr::unsynchronized_pool_resource を使った場合のメモリ使用量を減らす事が出来ませんでした。どうしてこんなにメモリ使用量が増えてしまうのか実装内容をちゃんと把握出来ていませんが、これだけ増えてしまうのでは使うのが厳しいなと判断して自前実装のメモリプールを使っています。

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1GiB余分に食ってしまう結果になったわけですね・・・。
検証にx64版を使っているのは、メモリプールだけで2GBを超えてしまうから、と・・・。

PRに含まれるコミットとこのあたりの資料を参考に、手元で少し検証してみようと思っています。
https://cpprefjp.github.io/reference/memory_resource/pool_resource.html

なんとなく「上流のメモリリソース」がキーワードな気がしています。

  • 上流のメモリリソース (確保単位ガバガバ)
    • CLayoutMgr のメモリリソース (確保単位=sizeof(CLayout))

技術的な詳細を把握しないままテキトー言ってるだけなので、試してみてやっぱり駄目かもしらんです。できるなら理想的な使い方をしたいのもありますし、できないならそれは暫定策を許容する理由になる気がします。

Copy link
Contributor Author

@beru beru Aug 13, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::pmr::unsynchronized_pool_resource の実装を確認したところ、_Find_pool メソッドで2の累乗サイズに切り上げしていました。

C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Tools\MSVC\14.16.27023\include\memory_resource の L639 ~

現時点で x64 Release ビルドでは sizeof(CDocLine) が 64 でそれに sizeof(void *) を足した値が 72 になり、切り上げた2の累乗値が 128 なので、1つのインスタンス毎に 128 バイトずつ使ってしまうのかもしれないです。 ←間違い、Debugビルドで確認してました。

今こんな気分です。

     l⌒Yl  lY⌒l   \メモリプールでメモリ節約!/ 
    { ´┴`} { ´┴`}        (^ν^) 
    ( | ̄ ̄|   )       /( )\ 
     | | ̄ ̄ ̄ ̄ ̄|       | | 

   は? 
     l⌒Yl  lY⌒l 
    { ´┴`} { ´┴`}        (^ν^) ・・・ 
    ( | ̄ ̄|   )       /( )\ 
     | | ̄ ̄ ̄ ̄ ̄|       | | 


     l⌒Yl  lY⌒l   \C++17 マンセー!!/ 
    { ´┴`} { ´┴`}        (^ν^) 
    ( | ̄ ̄|   )       /( )\ 
     | | ̄ ̄ ̄ ̄ ̄|       | | 

 ___ 
/ || ̄ ̄|| 
|.....||__|| (ν^ )  <ゲイシ組はアスペ 標準ライブラリ実装が豚の餌 
| ̄ ̄\三⊂/ ̄ ̄ ̄/ 
|    | ( ./     / 

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::pmr::unsynchronized_pool_resource ですが手持ちのプールの容量が足りなくなった時に _Increase_capacity メソッドで upstream から追加で Chunk を確保しますが、そのサイズ(_Next_capacity)を倍々で増やしています。これがメモリを浪費している原因じゃないでしょうか。

Copy link
Contributor Author

@beru beru Aug 14, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::pmr::unsynchronized_pool_resourceを削る前のコードって、「上流のメモリプール」を使ってなくないですか?

std::pmr::unsynchronized_pool_resource のコンストラクタには上流リソースを指定していません(デフォルトコンストラクタを使用)。その場合は std::pmr::get_default_resource() が使われる仕様と実装になっていますね。

アラインメントに指定できる値の要求仕様は「2の累乗であること」らしいです。

std::pmr::unsynchronized_pool_resource の実装だとアライメントだけじゃなくメモリプール内の個々の要素であるブロックのサイズも2の累乗にしているのでメモリを無駄遣いしてるように思えます。

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

テスト条件 16777216 行連続する からして、
確保サイズが1バイト無駄になるごとに 17MiB の領域が無駄になるということですよね。

アラインメントの関係で無駄になるサイズが100バイト近いとすれば
17MiB × 100倍 で 1GB が浪費される事態にも納得がいきます。
(そういう理屈の浪費なら1GB程度は許容範囲な気もします)

#988 CDocLine の容量削減 のような対応が必要なのかも知れません。

確保単位が2の累乗に切り上げられるということは、32bytesの上は64bytesになります。
現状の sizeof(CDocLine) が56bytesになるそうなので、
64 - 56 で1行あたり、8バイト無駄になる計算です。

ただ小さくすればいいという話ではなく、
CDocLineのサイズを2の累乗になるように調整する(無駄な確保を無くす)
という方向にすべきなんでしょうね。

std::pmr::get_default_resource() の戻り値は事前に set しない限り未定義、という仕様があるっぽいです。

ぼくが認識する限り、VirtualAllocは確保サイズに制約のある使い勝手の悪いメモリ確保関数です。裏をかえせば、通常よりも大きな単位で確保を行う「上流メモリリソース」の実装向きなんじゃないかと思います。

っていうか、このPRをマージできるようにするための「落としどころ」がどの辺にありそうか、よく分らなくなってきてるのはぼくだけですかね?

早い話、一旦妥協してもいいんじゃね?と思い始めています。

Copy link
Contributor Author

@beru beru Aug 14, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

テスト条件 16777216 行連続する からして、
確保サイズが1バイト無駄になるごとに 17MiB の領域が無駄になるということですよね。

アラインメントの関係で無駄になるサイズが100バイト近いとすれば
17MiB × 100倍 で 1GB が浪費される事態にも納得がいきます。
(そういう理屈の浪費なら1GB程度は許容範囲な気もします)

1GBもメモリ使用量が増えてしまうのは許容したくないです。2^24 行とかのファイルを開く事は通常は無いっちゃ無いですが。。

#988 CDocLine の容量削減 のような対応が必要なのかも知れません。

#988 では簡単に出来る対処で容量を減らしました。

32bit から 64bit になってポインタのサイズが 4バイトから 8バイトに増えたので、ポインタを多用してると結構使用メモリ量が増えてしまいます。

確保単位が2の累乗に切り上げられるということは、32bytesの上は64bytesになります。
現状の sizeof(CDocLine) が56bytesになるそうなので、
64 - 56 で1行あたり、8バイト無駄になる計算です。

ただ小さくすればいいという話ではなく、
CDocLineのサイズを2の累乗になるように調整する(無駄な確保を無くす)
という方向にすべきなんでしょうね。

インスタンス1つにつき 8バイト無駄になるだけでは、1GB のメモリ使用量の差は説明できません。現在のVC++付属の std::pmr::unsynchronized_pool_resource を使うとメモリ使用量が大幅に増えてしまう主要因は、追加のメモリ確保を行う方法にあるという理解です。

デバッグしやすいように Debug ビルドで、CLayoutMgr.cppCPoolResource の代わりに std::pmr::unsynchronized_pool_resource を使うように記述を切り替えてからCLayoutMgr::CreateLayout メソッドの先頭にブレークポイントを置いて下さい。

ビルドして起動した後に行追加操作を行うと先ほど追加したブレークポイントに止まるので、Step Into 操作を行ったりして memory_resource ヘッダのコードがデバッグ出来ます。

std::pmr::unsynchronized_pool_resource::_Increase_capacity メソッドの上から3行目がメモリ確保を行っている場所です。memory_resource ファイルの行番号でいうと、597 行目です。

その呼び出しのサイズを追ってみると下記のようになります。

n回目 _Size 行数
1 568 1
2 1080 5
3 2104 13
4 4152 29
5 8248 61
6 16440 125
7 32824 253

2倍2倍で確保する容量を増やしていく方式な事が確認出来ます。行数がやたらと多いファイルを開くと、途中で今まで確保した容量の合計以上の容量を一気に追加確保するのでそれでメモリ使用量が大きく増えてしまっているという事だと思います。

std::pmr::get_default_resource() の戻り値は事前に set しない限り未定義、という仕様があるっぽいです。

それは一体どこに書かれていた情報でしょうか?

https://en.cppreference.com/w/cpp/memory/get_default_resource
ここの説明を見る限りでは、std::pmr::new_delete_resource と同じみたいです。

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4713.pdf
の P.584 の先頭には下記の記載があります。

The default memory resource pointer is a pointer to a memory resource that is used by certain facilities when an explicit memory resource is not supplied through the interface. Its initial value is the return value of
new_delete_resource().

ぼくが認識する限り、VirtualAllocは確保サイズに制約のある使い勝手の悪いメモリ確保関数です。裏をかえせば、通常よりも大きな単位で確保を行う「上流メモリリソース」の実装向きなんじゃないかと思います。

性格的にメモリープールの実装に使うには丁度良いと思います。ヒープマネージャーがメモリ確保する為に使うAPIとしては、HeapAllocVirtualAlloc だと思いますが、余計なマネージメントをしてほしくない場合には VirtualAlloc が良いですね。

っていうか、このPRをマージできるようにするための「落としどころ」がどの辺にありそうか、よく分らなくなってきてるのはぼくだけですかね?

早い話、一旦妥協してもいいんじゃね?と思い始めています。

内部実装について考えるのが飽きてきたらブラックボックステストをするのはどうでしょうか?

  • 色々な動作確認をして問題のない挙動をしているか?
  • ファイル読み込みに掛かる時間は減っているか?
  • ファイル読み込み後のメモリ使用量は減っているか?

等で妥当かどうかの判断が出来ると思います。え?そんなのかったるいからPR作成者が徹底的にユニットテストを書けって?:sweat_smile:

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::pmr::get_default_resource() の戻り値は事前に set しない限り未定義、という仕様があるっぽいです。

それは一体どこに書かれていた情報でしょうか?

https://en.cppreference.com/w/cpp/memory/get_default_resource
ここの説明を見る限りでは、std::pmr::new_delete_resource と同じみたいです。

どっかの日本語サイトだと思いますが、覚えていないです。
未定義時はstd::pmr::new_delete_resourceの戻り値が返る、で間違いなさそうっすね。

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

どっかの日本語サイトだと思いますが、覚えていないです。

😦


Node* m_unassignedNode = nullptr; // 未割当領域の先頭
Node* m_currentBlock = nullptr; // 現在のブロック
Node* m_currentNode = nullptr; // 要素確保処理時に現在のブロックの中から切り出すNodeを指すポインタ、メモリ確保時に未割当領域が無い場合はここを使う
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

伝統C++と現代C++の違いの一つに「メンバ変数を書く位置」というのがあります。
なんとなく、伝統派が後置、現代派が前置なイメージを持っています。
これは仕様で規定されるものではないので完全な「好み」の問題なのでどっちでもいいと思っています。

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

間を取って中間ぐらいに書くべきでしょうか?

void AllocateBlock()
{
char* buff = reinterpret_cast<char*>(VirtualAlloc(NULL, BlockSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE));
Node* next = m_currentBlock;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

メモリ確保に失敗した場合はどうなるんですかね?

元のコードがそのケースを想定しとらんはずなので、
「知りません」でも構わんと思うんですけど、
シグニチャに例外の扱いを示すキーワードが含まれないのが気になりました。

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

元のコードだと new でのメモリ確保に失敗したら std::bad_alloc 例外が送出されるんでしょうかね。

このPRのコードだと VirtualAlloc でメモリ確保に失敗したら NULL が返ってその後ポインタを操作するので access violation になりそうです。

極限のケースに対する対処をちゃんと入れるかどうかですが、まぁ自分は入れる必要が無いなら入れないです。

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do_allocate が 上位のメモリリソースが例外を投げない限り例外を投げないで、
do_deallocate が 例外を投げないという仕様のようでした。

access violation で落ちるのはバグっぽくて感じ悪いと思います。
個人的には throw std::bad_alloc(); したいです。
最終的にユーザーにどう伝えるかを実装しないといけないんですが、それは別課題でいいように思います。

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

アプリが異常終了して作業データが失われてしまったら人権侵害な環境にいる人はストレスマッハになってしまうのでそのうち対策入れたいですね。

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

この要望、どうなる感じですかね?

現状)access violationで落ちる
提案)処理されない例外bad_allocのせいで落ちる

windowsが勝手に出してくれるダイアログで、違いは識別できるような気がするので入れて欲しいです。

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a97e50e で対応済みという認識です。

動作確認はしていません。

@beru
Copy link
Contributor Author

beru commented Aug 13, 2019

明らかにコードが原因でない問題の発生時は「一旦放置」で良いと考えています。
維持の責任をPR作成者に任せるつもりはありません。

あれ?今までPRの変更に付随してMinGWでビルドが通らなくなった場合に修正が求められるのがデフォだったような気がしてならない…。

@berryzplus
Copy link
Contributor

明らかにコードが原因でない問題の発生時は「一旦放置」で良いと考えています。
維持の責任をPR作成者に任せるつもりはありません。

あれ?今までPRの変更に付随してMinGWでビルドが通らなくなった場合に修正が求められるのがデフォだったような気がしてならない…。

つ「明らかにコードが原因でない問題の発生時は」

潜在的なコードの問題(主に字句文法)を検出するのがMinGWビルドの目的なので、コードに問題がないことが明らかな場合にまで対応をお願いする気はないです。今回はローカルでの検証ビルドは通っていて、引っ越ししたばかりの azure pipelines のビルドに問題がありそうです。

@AppVeyorBot
Copy link

Build sakura 1.0.2128 completed (commit 497bf32856 by @beru)

@AppVeyorBot
Copy link

Build sakura 1.0.2130 completed (commit 368b969a05 by @beru)

@AppVeyorBot
Copy link

Build sakura 1.0.2131 completed (commit b833c407af by @beru)

Copy link
Contributor

@berryzplus berryzplus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

お待たせしました。一旦コレでやってみよう、でハラが決まりました。


ラストだと思いますが、対応のお願いが1件あります。
プロジェクトファイルに今回追加する mem/CPoolResource.h を追加したいです。
GUIから既存ファイルを追加すると、sakura.vcxproj と sakura.vcxproj.filters に差分が出るはずなので。


あと、こっちは対応不要ですが、new/deleteを関数化する方法を手元で書いてみました。

※CDocLineMgr.h内の記述です。

private:
	//! 新しい物理行のメモリを確保して初期化する
	CDocLine* _AllocLine()
	{
		CDocLine* pcDocLineNew = nullptr;
		m_docLineAllocator.construct(pcDocLineNew);
		return pcDocLineNew;
	}

	//! 指定した物理行を破棄してメモリを解放する
	void _FreeLine(CDocLine * const pDocLine)
	{
		pDocLine->~CDocLine();
		m_docLineAllocator.deallocate(pDocLine, 1);
	}

細かいところは、まだまだだいぶ研究が必要っぽいです:smile:

@beru
Copy link
Contributor Author

beru commented Aug 15, 2019

お待たせしました。一旦コレでやってみよう、でハラが決まりました。

仮に後でこのPRの変更に問題が有る事が分かったら revert する事で元の状態には戻せるので、ざっと確認してみて問題が無ければ取り入れてしまって良いんじゃないかと思います。

きちんと品質確保をするのであればユニットテストを整備する必要がありますが、もし書くとしたら CDocLineMgr とか CLayoutMgr 側からもテストをしたいですね。

ラストだと思いますが、対応のお願いが1件あります。
プロジェクトファイルに今回追加する mem/CPoolResource.h を追加したいです。
GUIから既存ファイルを追加すると、sakura.vcxproj と sakura.vcxproj.filters に差分が出るはずなので。

91befa4 で対応しました。

あと、こっちは対応不要ですが、new/deleteを関数化する方法を手元で書いてみました。

ヘッダファイル側を変更してメソッドを追加する方法よりグローバル関数にする方が良い気がします。そのグローバル関数をどこに追加するかとかを考える必要が生じてしまいますが。。

@AppVeyorBot
Copy link

Build sakura 1.0.2140 completed (commit e8f6357dbd by @beru)

@berryzplus
Copy link
Contributor

プロジェクトファイルに今回追加する mem/CPoolResource.h を追加したいです。
GUIから既存ファイルを追加すると、sakura.vcxproj と sakura.vcxproj.filters に差分が出るはずなので。

91befa4 で対応しました。

確認しました。対応ありがとうございます。

お待たせしました。一旦コレでやってみよう、でハラが決まりました。

仮に後でこのPRの変更に問題が有る事が分かったら revert する事で元の状態には戻せるので、ざっと確認してみて問題が無ければ取り入れてしまって良いんじゃないかと思います。

とはいえ、ほぼノーチェックでやるならdevブランチとか切ったりしたくなるわけで、その辺のまとめをしてくれる人も現状はおらんので、とりあえずはgdgdとレビューっぽいことをしている感じです。

プロジェクトを見捨てて独自路線に走る手もありますが、それだと永久に合流できなくなる可能性高そうで微妙。

きちんと品質確保をするのであればユニットテストを整備する必要がありますが、もし書くとしたら CDocLineMgr とか CLayoutMgr 側からもテストをしたいですね。

ベースとすべき機能要求が不透明すぎるので、
品質保証とか実質的に無理だから、と思っています。←内緒の話

ユーザー視点で考えると、網羅率100%のユニットテストを用意しても「あ、そうなの」です。

興味があるのは、困っていた不具合が直ったかどうかだったり、要望した機能が要望通りに入ったかどうかだったり、変更によって今まで使えていた機能が使えなくなったりしてないかだったりだと思います。

これらを保証できる余地があるとすれば、ユニットテストではなくファンクションテストだと思います。

まーこの辺は、全力で「今後の課題」としたいところですけどね 😄

あと、こっちは対応不要ですが、new/deleteを関数化する方法を手元で書いてみました。

ヘッダファイル側を変更してメソッドを追加する方法よりグローバル関数にする方が良い気がします。そのグローバル関数をどこに追加するかとかを考える必要が生じてしまいますが。。

アロケータもリソースもprivate変数なので、private関数にする想定でした。
根元はグローバル関数にしてもいい気もしますが、引数が増えるのがデメリットですね。
あと多分、それってC++20で追加される関数のコピペになる・・・w

Copy link
Contributor

@berryzplus berryzplus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

というわけで、このメモリプールを使い始めたいと思います。
対応ありがとうございます。

@beru beru merged commit a28b4d5 into sakura-editor:master Aug 15, 2019
@beru
Copy link
Contributor Author

beru commented Aug 15, 2019

@berryzplus さん

レビューありがとうございました。もし問題が見つかったら別のPRで対処を行います。

@beru beru deleted the pool_resource branch August 15, 2019 09:44
@m-tmatma m-tmatma added this to the v2.4.0 milestone Dec 29, 2019
HoppingTappy pushed a commit to HoppingTappy/sakura that referenced this pull request Jun 16, 2020
メモリプールを使う事でメモリ確保と解放を高速化
This pull request was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants