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

行データ管理クラスでメモリプールを使う事でメモリ確保と解放処理を高速化 #980

Closed
wants to merge 10 commits into from

Conversation

beru
Copy link
Contributor

@beru beru commented Aug 3, 2019

PR の目的

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

カテゴリ

  • 速度向上

PR のメリット

(行数が)極端に大きいファイルを開く時の待ち時間が短くなります。

あと分かりづらいですがそのファイルを閉じる時の時間も短くなります。

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

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

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

メモリ使用量が思ったより減らないといった事態が考えられます。

メモリプールのメモリ解放のタイミングが行データの管理クラス(CDocLineMgr)が解放されるタイミングなので、エディタのプロセスを閉じるまでメモリプールのメモリ解放がされない事になります。

ただそれで困るという事はあんまり考えられない気もします。

PR の影響範囲

行データ管理クラス(CDocLineMgr)関連

関連チケット

#312

過去に #312 で同様の事を行いましたが、外部プロジェクトのソースコードを使っていたので取り込むのに障害が有りました。今回は同様の処理を自前で書いているのでその問題は無いです。

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

Build sakura 1.0.2094 failed (commit f90149d767 by @)

@berryzplus
Copy link
Contributor

#979 の事象に巻き込まれて appveyor の MinGW ビルドがコケてますね 😢
#981 マージ後に rebase すればエラーは解消すると思われます。

#include <memory>
#include "util/design_template.h"

template <typename T, size_t BlockSize = 4096>
Copy link
Member

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.

追加しました。クラスのI/Fを意図的にミニマムにしたので試験内容も単純なものになっています。

@AppVeyorBot
Copy link

Build sakura 1.0.2097 failed (commit 0b5d19fc76 by @beru)

@AppVeyorBot
Copy link

Build sakura 1.0.2098 failed (commit 07882dd744 by @beru)

@AppVeyorBot
Copy link

Build sakura 1.0.2099 failed (commit b24caab739 by @beru)

@AppVeyorBot
Copy link

Build sakura 1.0.2100 failed (commit 2d2da1145a by @beru)

@AppVeyorBot
Copy link

Build sakura 1.0.2101 completed (commit 1e024d0471 by @beru)

}

private:
union Node {
Copy link
Member

Choose a reason for hiding this comment

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

ここでなぜ union を使用しているのですか?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

下記の2つの理由が考えられます。

  • 領域を再利用する為に要素型と自己参照用のポインタを同じ領域に割り当てる
  • 共用体のサイズは各メンバを格納できるサイズになる事を利用

Copy link
Member

Choose a reason for hiding this comment

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

自己参照用のポインタと要素型を同じ領域に配置して問題ないのですか?
可能であれば、アスキーアート(あるいは SVG とか)でデータ構造の説明が欲しいです。

Copy link
Member

Choose a reason for hiding this comment

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

134行目の
body = std::align(alignof(Node), sizeof(Node), body, space); からすると

こんな感じ?


     +----------------------------------+
     |                                  V
+-----------+---------+------------+-----------+---------+------------+
| ポインタ  |         |            | ポインタ  |         |            |
|   next    | padding | T のデータ |   next    | padding | T のデータ |
|           |         |            |           |         |            |
+-----------+---------+------------+-----------+---------+------------+
                      ^                 |                                 ^
                      |                 +---------------------------------+
              タイプ T のサイズ向けに align

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 Author

Choose a reason for hiding this comment

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

@m-tmatma さん

可能であれば、アスキーアート(あるいは SVG とか)でデータ構造の説明が欲しいです。

図をPlantUMLとかで用意する事は出来ますが、行動に移すのを躊躇する何かが有ります。

今回作成したメモリプールの実装は100行未満の低機能な単純なものなので、分かりやすいコードの書き方にする事に注力した方が良いかも知れません。

Copy link
Member

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.

現状のコード読んで作りが理解できないですか?それならこのPR自体諦めますけど。

Copy link
Member

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 "util/design_template.h"

template <typename T, size_t BlockSize = 4096>
class CMemPool final
Copy link
Member

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.

コメントを記載しました。

sakura_core/mem/CMemPool.h Outdated Show resolved Hide resolved
Node* next;
};

static_assert(BlockSize >= 2 * sizeof(Node), "BlockSize too small.");
Copy link
Member

Choose a reason for hiding this comment

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

ここの 2 にはどういう意味がありますか?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ブロック領域のサイズがNode2個分以上でなければいけない、という確認です。
何故 2 なのか?という疑問が湧きますが、あんまり厳密なチェックではないと思います。

  • 要素型のサイズがポインタのサイズ以下のケース(Nodeのサイズはポインタのサイズ以上)
    • ブロック領域の先頭にはポインタが来ます。残りの領域のサイズも最低ポインタ分が無いと解放時に未割当領域に出来ません。ブロック領域のサイズがNode2個分以上ならそれを満たします。
  • 要素型のサイズがポインタのサイズより大きいケース(Nodeのサイズは要素型のサイズ以上)
    • ブロック領域の先頭にはポインタが来ます。残りの領域のサイズは Node 1つ以上有れば良いですが、ブロック領域のサイズがNode2個分以上なら条件満たします。

ケースを限定すればもっと必要なサイズを絞れる気もしますが、まぁ気張らないで今のやり方でも良いとは思います。

Copy link
Member

Choose a reason for hiding this comment

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

コメントには Node2個分以上 にする理由を記載いただきたいです。
Node2個分以上というのは見たらわかるので。

Copy link
Contributor Author

Choose a reason for hiding this comment

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

記載を更新しました。

return ret;
}
else {
Node* border = reinterpret_cast<Node*>(reinterpret_cast<char*>(m_currentBlock) + BlockSize - sizeof(Node) + 1);
Copy link
Member

Choose a reason for hiding this comment

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

ここの +1 はどういう意味ですか?

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 の大きさ未満だともうそのブロック領域からは Node を切り出す事が出来ません。それを判定する際に使うポインタ変数のアドレス計算です。

Copy link
Member

Choose a reason for hiding this comment

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

データ構造はまだ理解できていませんが、
このコードを見たときに、 99 行目で +1 しているのは 100 行目で >= にしているからで
100 行目で > にすれば 99 行目の +1 はいらないんじゃないかと思ってコメントしました。

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
Member

Choose a reason for hiding this comment

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

細かいこと言うと、+1 の演算が余計に入るので現状コードのほうが少し遅い。

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
Member

Choose a reason for hiding this comment

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

もうちょっとロジックを理解してから再度コメントするか考えます。

}

// ブロックサイズ
TEST(CMemPool, BlockSize)
Copy link
Member

Choose a reason for hiding this comment

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

このテストは何をテストしようとしていますか?

Copy link
Member

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 (comment) について確認するテストです。

Copy link
Member

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.

コメントを追加しました。

@m-tmatma m-tmatma added this to the v2.4.0 milestone Aug 4, 2019
@berryzplus
Copy link
Contributor

所感

ぼんやりと眺めてる限り、いい感じだと思います。
ぼくの中の一時見解は「しばらく保留」です。

保留としたい理由

影響範囲
行データ管理クラス(CDocLineMgr)関連

CDocLineMgr は編集中のドキュメントデータを管理するクラスです。

ちなみにデータ管理系クラスとエディタとの関連はこんな感じ。

CDocLineMgr (行データ管理クラス) ← コアです。
   ↓
CLayoutMgr (レイアウト管理クラス)
   ↓
CEditView (編集ビュー) ← 編集ペインの一区画を表現するものです。
   ↓
CEditWnd (編集ウインドウ) ← 一般ユーザーが「サクラエディタ」と認識するものです。

CDocLineMgr に対する変更は、 サクラエディタの編集機能全般 に影響します。
なので、変更は最大限慎重に吟味する必要がある気がします。

変更内容をキチンと理解できるまでは保留としたいです。

どうなったらOKと言えるか?

PR全文を頭から読み返してみました。

目的「ログファイル等の行数が多いファイルを読んで表示するのに掛かる時間を減らす。」
  ↓変更内容とガッチャンコして行間を補完すると・・・
目的「巨大ファイルを開いたとき編集できるようになるまでの時間を短縮したい」
手段「メモリプールを導入(=メモリ確保のコストを落とす)して高速化する」
※ぼくの勝手な理解なのでズレていたら訂正お願いします。

まず、目的と手段があってるかについて自信がないっす。

「巨大ファイルを開くのに時間がかかるのは、メモリ確保の速度が遅いからだ」という検証結果が欲しいです。常にそうでなくて構わないのです。ファイルオープンが遅い理由は他にもたくさんあるので、他の理由との兼ね合いで速度が出ない状況もありえると思います。

特定の状況で、メモリ確保が遅いことによりファイルオープンが遅くなる、が証明できれば細かいことは気にしなくていいのかな、と思っています。

あ、単体テストはぼくも欲しいです 😄

単体テストがあると、追加されるクラスが何を意図して作られたものかが明確になるからです。
※テストは「~ならOK、~ならNG」の書き方なので自然とそうなると思います。

@berryzplus
Copy link
Contributor

ああ、失礼。
検証結果は前の PR にありましたね。

#312 (comment)

a\r\n という行が 16777216行連続している 48MB のファイルを読み込む時間が 7.5 秒から 5.8 秒になりました。

@AppVeyorBot
Copy link

Build sakura 1.0.2102 failed (commit e0c384fbe5 by @)

@beru
Copy link
Contributor Author

beru commented Aug 4, 2019

「巨大ファイルを開くのに時間がかかるのは、メモリ確保の速度が遅いからだ」という検証結果が欲しいです。常にそうでなくて構わないのです。ファイルオープンが遅い理由は他にもたくさんあるので、他の理由との兼ね合いで速度が出ない状況もありえると思います。

要素ごとに分析して個々に処理時間測定を行う事はしていません。CDocLine のメモリ確保に時間が掛かっていてメモリプールを使う対策を入れる事にしたのは、多分過去に Visual Studio の Performance Profiler で確認したからとかかもしれないです。

メモリプールを使う事で行数がとても多いファイルを開く処理の時間が短くなる(メモリ使用量も減る)事は、そういうファイルを開く操作を行えば知覚出来るぐらいなので、あえてプログラムを改造したりしてベンチマークを取らないでも良いかなと思ってます。

まぁ結構適当なスタンスではあります。。問題が無い事を自分で色々な角度でしっかりと確認して検証すべきなのはそうなんですが、ヘタレなんでお手柔らかにお願いします。

@AppVeyorBot
Copy link

Build sakura 1.0.2103 completed (commit e0ad569349 by @)

@AppVeyorBot
Copy link

Build sakura 1.0.2105 completed (commit 7689d57a9f by @)

@beru
Copy link
Contributor Author

beru commented Aug 4, 2019

@berryzplus さん

所感

ぼんやりと眺めてる限り、いい感じだと思います。
ぼくの中の一時見解は「しばらく保留」です。

保留としたい理由

影響範囲
行データ管理クラス(CDocLineMgr)関連

CDocLineMgr は編集中のドキュメントデータを管理するクラスです。

ちなみにデータ管理系クラスとエディタとの関連はこんな感じ。

CDocLineMgr (行データ管理クラス) ← コアです。
   ↓
CLayoutMgr (レイアウト管理クラス)
   ↓
CEditView (編集ビュー) ← 編集ペインの一区画を表現するものです。
   ↓
CEditWnd (編集ウインドウ) ← 一般ユーザーが「サクラエディタ」と認識するものです。

CDocLineMgr に対する変更は、 サクラエディタの編集機能全般 に影響します。
なので、変更は最大限慎重に吟味する必要がある気がします。

CDocLineMgr の単体テストが無いので、この変更によって問題が生じて無いのか分かりづらいんですよね。

@m-tmatma
Copy link
Member

m-tmatma commented Aug 4, 2019

CDocLineMgr に対する変更は、 サクラエディタの編集機能全般 に影響します。
なので、変更は最大限慎重に吟味する必要がある気がします。

変更内容をキチンと理解できるまでは保留としたいです。

CDocLineMgr の単体テストが無いので、この変更によって問題が生じて無いのか分かりづらいんですよね。

#780#948 が発生したというのかありましたしね。(#949 で修正)

class CMemPoolTest : public ::testing::Test{};

using test_types = ::testing::Types<
std::integral_constant<std::size_t, 512>,
Copy link
Member

Choose a reason for hiding this comment

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

BlockSize は 2の累乗である必要はありますか?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

2の累乗値である必要はありません。

Copy link
Member

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.

2の累乗値ではない値も追加しました。

@AppVeyorBot
Copy link

Build sakura 1.0.2107 completed (commit b7619f4534 by @beru)

// ブロックサイズは要素が2つ以上入る大きさにする確認
TEST(CMemPool, BlockSize)
{
CMemPool<std::array<uint8_t, 1024>, 2048> pool0;
Copy link
Member

Choose a reason for hiding this comment

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

クラスをクラスタンス化するだけだと、AllocateBlock が呼ばれるだけですが、
Construct や Destruct を呼ぶ必要はないですか?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

色々な要素型の場合にBlockSizeを最小限の大きさにした場合に正常にCMemPoolが動作するかの確認をした方が良さそうですね。

@AppVeyorBot
Copy link

Build sakura 1.0.2110 completed (commit e695e2c0f1 by @beru)

@AppVeyorBot
Copy link

Build sakura 1.0.2113 completed (commit 3ab2590765 by @beru)

@m-tmatma
Copy link
Member

m-tmatma commented Aug 4, 2019

データ構造は
ブロックの先頭にポインタがあって
その後にパディングがあって
その後に T のデータの配列が続く

という構造でしょうか?

それなら
上記の構造をコメントに書いた上で、
構造体使わずに単に char 型 または uchar 型のポインタを
保持して、ポインタを加算してデータにアクセスする
ようにした方がシンプルになると思います。

@beru
Copy link
Contributor Author

beru commented Aug 4, 2019

データ構造は
ブロックの先頭にポインタがあって
その後にパディングがあって
その後に T のデータの配列が続く

という構造でしょうか?

そうです。パディングは常にあるわけではなく T のデータの配列のアライメントを取るのに必要な場合にのみ存在します。 8110e42 で構造体を使うように書き方を変更しましたが、構造体の各メンバーのアライメントは自動で取られるので、ポインタ操作の記述が省けます。

それなら
上記の構造をコメントに書いた上で、
構造体使わずに単に char 型 または uchar 型のポインタを
保持して、ポインタを加算してデータにアクセスする
ようにした方がシンプルになると思います。

その考えには同意できないです。コードを提示してもらえば @m-tmatma さんが言うシンプルさというのがどんなものなのかを検討してみますが。

~Block() {}
struct {
Block* next;
Node nodes[1];
Copy link
Member

Choose a reason for hiding this comment

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

もともとのコードではここに padding 入れてませんでしたか?
コンパイラにお任せですか?

Copy link
Contributor Author

@beru beru Aug 4, 2019

Choose a reason for hiding this comment

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

はい。もともとのコードでは、CMemPool::AllocateBlock メソッドで std::align 関数を使ってアライメントを取っていましたが、構造体を使う事でコンパイラに任せるようにしました。

Copy link
Member

Choose a reason for hiding this comment

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

static_assert 等などで、それを保証した方がいいと思います

Copy link
Contributor Author

@beru beru Aug 4, 2019

Choose a reason for hiding this comment

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

変数のアドレスは動作時に決まるものなので、静的には保証出来ないのではないかと思い当たりました。

なので行うべきは CMemPool::AllocateBlock プライベートメソッドにおいて operator new で確保した領域を Block* にキャストして m_currentBlock メンバーに割り当てる際に、std::align を使って確実にアライメントが取られるようにする事、な気がします。

単体テストでももっと大きいアライメントを要求する型を用意して確認する事にします。

union Node {
~Node() {}
T element; // 要素型
Node* next; // 解放後の未割当領域の場合は次の未割当領域に繋がる
Copy link
Member

Choose a reason for hiding this comment

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

element が有効か、next が有効かはどんなロジックで
判断されますか?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  • element として扱う場合(next としては扱わない)
    • CMemPool::Construct メソッドが呼び出されるとそこから CMemPool::Allocate プライベートメソッドを呼び出して割り当てる領域(Node)の要素(Element)のアドレスを返します。それ以降は破棄されるまではその領域は要素として扱われます。
  • next として扱う場合(element としては扱わない)
    • CMemPool::Destruct メソッドが呼び出されると要素のデストラクタ呼び出しを行って破棄し、Nodeを未割当の領域として扱うようになります。要素として扱わなくなった時点で領域 を自己参照型として扱えるようになります。

Copy link
Member

@m-tmatma m-tmatma Aug 4, 2019

Choose a reason for hiding this comment

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

観点が逆です。
element として扱う場合とnext として扱う場合の
場合分けをする判断基準が知りたいです。

心配してるのは、element のデータを next とみなしたり
その逆になるパターンがないか?です

Copy link
Contributor Author

Choose a reason for hiding this comment

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

もしそういうパターンに陥るケースがある事に気づいたりコードにロジック上の不備を見つけたのならそれを指摘して下さい。延々と体裁とかに関する要求をされて不毛な感じです。

メモリプールクラスを汎用的にして品質を高める事に注目したいわけではなくて、パフォーマンスを改善したかったんですが…。

sakura-editor/management-forum#8

を再度読んでください。

Copy link
Member

Choose a reason for hiding this comment

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

コメントを書いてほしいというのは
別に体裁に関するコメントではありません。

@beru さんの考えているより、このクラスは
簡単ではないと思います。

最初にデータ構造をしめす説明をするなり
コメントなりがあるとレビューする側の時間を
節約できます。

PRを作成する人は、多くの時間を割いていますが、
同時に レビューする側も同様に時間をかけています。

レビューする側の時間を節約するという観点も
持って頂けると有難いです。

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
Member

Choose a reason for hiding this comment

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

コメントで説明をごてごてに書く事よりコードの記述に注力しないと結局出来上がるものに寄与しません。

まず認識していだだきたいのは、レビューに時間を割いているのは
PR に期待しているからです。

大して価値のない PR ならレビューに時間はかけないです。
価値のあるものだから、よりよくしようと意見を言うわけです。

コメントというのは単に手段であって目的ではないです。
コメントという形をとっていますが、どちらかというと
設計および仕様を残してほしいという意図です。

この PR がマージされたあと、コードを書いた人とは別の人が容易に
変更できることを目的にしてコメントを書いてほしいといっています。

データ構造の説明って最初からメモリプールって書いてるじゃないですか…。

メモリプールといってもどういう構造をとるかはいろいろあると思います。

コード読んで分からなかったら頭の中でシミュレートしたり実際にビルドして動かしてロジックの内容をつかんでからレビューコメント書くべきですよ。

コメントを書くのは嫌なのは想像しますが、
レビューアーがレビューしやすいような PR を作成するのは重要ですよ。
そうすることで早くマージされる可能性が上がるとおもいます。

メモリプールクラスを汎用的にして品質を高める事に注目したいわけではなくて、パフォーマンスを改善したかったんですが…。

パフォーマンスを改善するために、バグを生んでは元も子もないので、
基本的な部分で使われるクラスに関しては、バグのないようにするのか
基本かと思います。

Copy link
Contributor Author

Choose a reason for hiding this comment

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

観点が逆です。
element として扱う場合とnext として扱う場合の
場合分けをする判断基準が知りたいです。

まず当たり前な前提として、element として扱うのか、それとも next として扱うか、というフラグ的な記録は有りません(用意する実装方法も考えられますが)。にもかかわらずライフサイクルにおいて element として扱われるか、それとも next として扱われるかが変化しています。メモリを参照するロジックの記述場所によって扱いが別れているわけです。

CMemPool クラス側のメソッド内の記述では、どちらとして扱っているのかはコード上に現れています。ただどうしてそういう記述にしているのか?が不明瞭なのかもしれません。

未割当領域と呼んでいるのは、CMemPool::Destruct の呼び出しが行われた領域ですが、それらは next として扱います。どうして next として扱えるのか?についてですが、解放して element としての役割をいったん終えた為にその領域を next として扱えるからです。「扱えるからと言ってどうして扱うのか?」ですが未割当になったNodeを next メンバーを使って連結して後で要素構築時にその未割当領域から確保するようにしてメモリの再利用をするからです。再利用をやっておかないと、確保と破棄を繰り返すうちにメモリ使用量がどんどん増えてしまうのでこれは必要な事です。

次に element として扱う場合ですが、CMemPool::Construct の呼び出しが行われてそこで CMemPool::Allocate の呼び出しを行って返却された領域を element として扱います。「どうしてそれらを element として扱うのか?」というのはなんだか微妙な問いに思えますが、まず確保したブロック領域の中を切り出して element として利用しないとメモリプールとして成り立たないのでそうします。では問いを変えて「どうしてそれらの領域は element として利用出来るのか?」ですが element として利用する間は next は扱わないライフサイクルに入るからです。一つ前の段落とは裏返しですね。

さて、上記の説明ですがそんなん答えになっていないぞと言われるとちょっと禅問答なコースに入りそうですが…、まぁもしそう思われるとしたら私の説明が至らないせいでしょうか…。

心配してるのは、element のデータを next とみなしたり
その逆になるパターンがないか?です

素朴ですが良い質問だと思います。

そういう事が絶対に起きえない、とは言えないと思います。どういう時に起き得るかというのを考えてみると、

  • CMemPool クラスの利用者側(直接的には CDocLineMgr クラス)がポインタの扱いを不適切にして、本来扱うべきではない領域までメモリの書き込みをしてしまう場合
    • これについては生ポインタがどしどし使われているので、気を付けて下さい、としか言えません。
  • CMemPool::Destruct に不正なポインタが渡されるケース
    • 不正なポインタというのは、CMemPool::Construct メソッドが返したポインタではないアドレスのポインタや、また以前に CMemPool::Destruct を呼び出して破棄済みなのに再度続けて CMemPool::Destruct を呼び出す等した場合
    • そもそも CMemPool 利用者側が変なコードを書くべきではありませんが、CMemPool 側で取れる対策としては自分が確保したブロック領域群のアドレスの範囲内かどうかを確認するコードをデバッグビルドの時にチェック用に入れる、等が考えられます。
  • CMemPool::Destruct を呼び出した後にその領域は既に破棄済み扱いなのに、CMemPool 利用者側が element として扱ってしまう場合
    • これについても CMemPool 利用者側が変なコードを書くべきではありませんが、CMemPool 側で取れる対策としては、デバッグビルドでは 0xcdcdcdcd 等のパターンで next ポインタ以外の領域を初期化するコードを CMemPool::Free に入れるとかでしょうか?

さてやたらと回答が長文になってしまいましたが、文章力が貧弱なせいかもしれません。とほほー。

Copy link
Contributor Author

Choose a reason for hiding this comment

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

まず認識していだだきたいのは、レビューに時間を割いているのは
PR に期待しているからです。

大して価値のない PR ならレビューに時間はかけないです。
価値のあるものだから、よりよくしようと意見を言うわけです。

ありがとうございます。

コメントというのは単に手段であって目的ではないです。
コメントという形をとっていますが、どちらかというと
設計および仕様を残してほしいという意図です。

ソースコードも設計および仕様の一形態ですが、C/C++は形式言語ではないですし静的解析も行いにくいのでそれだけでは限界があるかもしれないですね。

@m-tmatma さんの要求って、レビューアーが細部を理解出来る事は当然として、バックグラウンドが様々な第三者が見ても内容がすっきりと分かるくらいの説明を残してほしいという事ですよね?それは理想的かもしれないですけど、そこまで本当に必要なんでしょうか?

分かる人には分かる、的なコードばっかりだとメンテが確かにきつくなるとは思いますけど、ドキュメント文化におんぶにだっこしたがりマンばっかりだとそれもなぁ…。

この PR がマージされたあと、コードを書いた人とは別の人が容易に
変更できることを目的にしてコメントを書いてほしいといっています。

なんだかその調子で行くと、拡張性が足りない、汎用性が無い、とか色々な要求も上乗せされそうですね…。

メモリプールといってもどういう構造をとるかはいろいろあると思います。

そりゃそうでしょうけど、教材を作っているんじゃないんだし懇切丁寧なコメントとテストとドキュメントを揃えないと取り込めない、ってなるとキッツいです。

コメントを書くのは嫌なのは想像しますが、
レビューアーがレビューしやすいような PR を作成するのは重要ですよ。
そうすることで早くマージされる可能性が上がるとおもいます。

そうですねぇ。レビューアーは審査官みたいなものですね。

パフォーマンスを改善するために、バグを生んでは元も子もないので、
基本的な部分で使われるクラスに関しては、バグのないようにするのか
基本かと思います。

私も他力本願ですが、レビューアーがコメントが無いコードを読解してバグを指摘してくれたら楽だなぁ、と思います。

@beru beru closed this Aug 4, 2019
@beru beru deleted the CDocLineMgr_mempool branch August 4, 2019 22:53
@berryzplus
Copy link
Contributor

berryzplus commented Aug 6, 2019

テンプレートを利用したメタプログラミングの技法を使えば、メモリプール内の割り当て済み領域と未割当領域を「型」で区別できるようになります。ただ、それをやるとこれほどシンプルな修正にはならんです。

virtual_allocator(メモリプール機能付きメモリアロケータ) とか作ってみたことあるんですが、適用するための修正箇所が膨大過ぎて PR する気にもならんかったです。出すとたぶん「boostで良くね?」とか、結論を出しづらいメンドクサイ展開になりそうなのでまだ当分保留するつもりです 😢 メモリプールの実装概念自体それなりに難解なので、「CMemPoolさえ理解できればなんとかなる」ようにまとめたこの修正の価値は大きいと思います。

このPRは閉じてしまってるので一旦終了なんですけど、今いるメンバーで C/C++ に一番詳しいのはたぶん @beru さんなので、また力を貸していただけると幸いに思っとります 😄

@beru
Copy link
Contributor Author

beru commented Aug 6, 2019

@berryzplus さん

テンプレートを利用したメタプログラミングの技法を使えば、メモリプール内の割り当て済み領域と未割当領域を「型」で区別できるようになります。ただ、それをやるとこれほどシンプルな修正にはならんです。

virtual_allocator(メモリプール機能付きメモリアロケータ) とか作ってみたことあるんですが、適用するための修正箇所が膨大過ぎて PR する気にもならんかったです。出すとたぶん「boostで良くね?」とか、結論を出しづらいメンドクサイ展開になりそうなのでまだ当分保留するつもりです 😢 メモリプールの実装概念自体それなりに難解なので、「CMemPoolさえ理解できればなんとかなる」ようにまとめたこの修正の価値は大きいと思います。

C++20 になればプリミティブなライブラリを用意せずとも標準ライブラリのもので間に合うケースが増えそうですね、自作が必要なケースは決して無くなりはしないでしょうけれど。

レガシーコードの書き換え自体も大変だと思いますが、テストも合わせて用意しないと挙動を壊してしまった事に気づきにくいので容易な道では無いと思います。

言われてみると allocator クラスを作るのが普通のC++ですね、このPRではサクラエディタの他のクラスに足並みを揃えたクラス名にして、あと利用ケースが限定されているので余計なI/Fは付けずに実装もミニマムにしてみました。しかしOOPの体面を取り繕おうとして CMemPool クラスとして独立させてしまったのが失敗の元だったとも思います。他からも使える余地が生じてしまうと、他者が理解&メンテしやすいようにという建前金科玉条に従いしっかりと整備を求められるコースに強制的に遷移する事が分かりました。歴史に if (未練は良くない)ですが、CDocLineMgr クラスのメンバーに前方参照した型の3要素のポインタ配列を追加して、CDocLineMgr.cpp 側のいくつかの static inline 関数で実装するべったり結合する方式にした方が大分良かったかもしれません。

このPRは閉じてしまってるので一旦終了なんですけど、今いるメンバーで C/C++ に一番詳しいのはたぶん @beru さんなので、また力を貸していただけると幸いだと思っとります 😄

自分が一番詳しいとかせもまつですね。きっと将来こんな感じですね。
https://twitter.com/beeple/status/1158234818242473984

@berryzplus
Copy link
Contributor

このPRによる修正でファイル読込が速くなる理由について解説しときます。

CDocLineMgr は、いわゆる 双方向リンクリスト を独自に実装したクラスです。c++で連結リストを扱うときは std::list<T> を使うのが定石ですが、サクラエディタは元々C言語で書かれているので、リンクリストの実装がC言語方式になっています。C言語のリストは、リスト要素を構造体(c++で言う、全メンバpublicなうんこクラス)として定義し、next, previousのポインタメンバを持たせることで実現します。この方式自体に特に問題はありません。

PRで改善されるのは、ファイル内の行数が極端に多い場合の読込速度です。
ファイルサイズ自体とは関係なく、純粋に行数が多い場合の速度が改善します。

なぜか?

CDocLineのためのメモリ確保を小さなサイズで個別に行っていたのを、大きなサイズでまとめて行うように変えたからです。ヒープマネージャーに対する問い合わせの回数が減った分だけ速度が改善したものと考えられます。

メモリ要求 (param: サイズ)
 ↓
ヒープマネージャー
 要求サイズが大きいときは新たなページを確保して返す ←この処理は結構速い
 要求サイズが小さいときは既存ページの空きをチェックして(ry ←この処理が重い

ヒープのメモリ確保は実際にはヒープサイズ単位で行われる。通常4Kごと。
ページとはシステム依存の仮想メモリ確保単位で、通常64Kごと。

ページサイズを超えない範囲でヒープサイズの整数倍となるようなサイズを指定すれば効率よくメモリ確保できるはず。

@beru
Copy link
Contributor Author

beru commented Aug 10, 2019

@berryzplus さん

細かい点ですが自分の理解と食い違っている点をコメントします。

ページサイズは通常 4KiB だそうです。

あとWindowsAPI の VirtualAlloc 関数で確保した仮想メモリ領域のアドレスは 64KiB 単位にアライメントされてるようです。
自分の環境で下記のコードで確認しましたが確かにそうでした。

SYSTEM_INFO si;
GetSystemInfo(&si);
assert(si.dwPageSize == 4096);
assert(si.dwAllocationGranularity == 1024*64);
for (size_t i=0; i<=1024*64*2; ++i) {
	LPVOID p = VirtualAlloc(NULL, i, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
	assert(((ptrdiff_t)p % (1024*64)) == 0);
	VirtualFree(p, 0, MEM_RELEASE);
}

なんでページサイズが4Kなのに仮想アドレスが64KでアライメントされているのかについてはMSの有名な開発者 Raymond Chen さんのブログに書かれていて、Alpha AXPプロセッサの命令セットでも効率よく動かすためにそうしたみたいです。

一般的なヒープマネージャーの処理内容については自分で1から実装出来るほどきちんと詳細には把握していませんが色々なメモリ確保と解放の使用状況に対応する為に結構様々な処理をやっていると思います。自前のメモリプールでは用途を限定して単純な事しか行っていないのでその分だけ処理時間が短くなるのだと思います。

@berryzplus
Copy link
Contributor

細かい点ですが自分の理解と食い違っている点をコメントします。

ページサイズは通常 4KiB だそうです。

ウソ書きました。
メモリの確保がある程度大きなまとまりで行われることが伝わればいいかな、と思って言葉の端々の正確さは気にしていませんでした。

タバコの発注単位に置き換えると、1本とか1箱(=20本)じゃなくて、
1カートン(=10箱=200本)とか1段ボール(=50カートン=500箱=10000本)で発注しますよ、という感じ。

メモリ確保単位はシステムによって違うことになっているので、const切らずに実行時にGetSystemInfoするのが定石っぽいです。新しいPRでメモリ確保単位が constexpr になってるのを見て「ぬ?」と思ったことはまだ秘密です。

@beru
Copy link
Contributor Author

beru commented Aug 11, 2019

ウソ書きました。
メモリの確保がある程度大きなまとまりで行われることが伝わればいいかな、と思って言葉の端々の正確さは気にしていませんでした。

タバコの発注単位に置き換えると、1本とか1箱(=20本)じゃなくて、
1カートン(=10箱=200本)とか1段ボール(=50カートン=500箱=10000本)で発注しますよ、という感じ。

CRTのヒープマネージャーやOSヒープは汎用的に作られていて比較的遅いのでそれらを叩く回数を減らす事で速くする、という点は上記の例えでイメージは伝わるかもしれないですね。

付け加えていうと、まとめて確保したメモリの管理を簡易的なメモリプールが受け持ちますが、それがやる仕事内容は単純な為にすぐに応答を返せるのでたくさん呼び出しが行われても比較的早く片付きます。

無理やり例えていうと遠くにある喫茶店やレストランとは違って、自販機や給水機は単純だけど速い、とかでしょうか?

メモリ確保単位はシステムによって違うことになっているので、const切らずに実行時にGetSystemInfoするのが定石っぽいです。新しいPRでメモリ確保単位が constexpr になってるのを見て「ぬ?」と思ったことはまだ秘密です。

ハードウェアアーキテクチャが異なると VirtualAlloc による最小メモリ確保単位が 64K じゃない事もあるのかもしれないですね。x86/x86-64 では 64K なので決め打ちでも良い気もしますけれど…。

新しいPRでメモリ確保単位をテンプレート引数で指定するのではなくて 64K 固定にしたのは、指定出来ないと困る用途は今のところ無いと判断したからです。

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.

4 participants