技術文章>テキストエディタ実装技術その2
前稿で、 ピーステーブルデータ構造を用いたバイナリエディタ用仮想バッファ第1版のC++による実装と評価結果について報告した。 筆者環境での総合処理速度測定結果は gap_vector を用いた版の約1/10 という燦々たるものであった。 しかし、バイナリエディタ用ということで、バッファサイズが変化しない場合についてのみ測定を行った上に、 テキストエディタ用バッファとして必須の undo/redo 機能を実装しておらず、ピーステーブルデータ構造の長所がでづらい比較であった。 そこで本稿ではバッファサイズが変化する場合についても評価を行うとともに、 undo/redo 機能の実装方法を説明し、 それを実装した場合の評価結果について報告する。
目次(予定所要時間):
1次元のテキストデータ(シーケンスデータ)を管理するクラス。
基本機能:内容参照、編集(挿入・削除)
テキストエディタのエンジンにあたる部分で、必要十分な機能・高品質・高速・高メモリ効率・拡張性が求められる。
行単位双方向リンク(vi)、ギャップバッファ(emacs)などがあり、それぞれ一長一短がある。
ギャップバッファ(= gap_vector<uchar>)はメモリ効率・処理速度の両面で非常に優れたデータ構造だが、
メインメモリ容量以上のテキストを処理できないという問題がある。
64ビットOSであれば4GB以上のバッファ容量を確保可能であるが、データ移動のたびにスワップ処理が発生し、
パフォーマンスが著しく低下する可能性がある。
シーケンスコンテナを階層的に組み合わせることで新たな特徴をもつデータ構造を作成でき、仮想バッファ化も可能になる
例:行単位双方向リンク、ピーステーブルなど
テスト容易性・拡張性・可換性を高めるために、STL に準じたインタフェースを持つのがよいと考える。
テンプレートによるジェネリックな実装はクラスの再利用性を高めるとともに、クラスのソフトウェアIC化にも大変有効である。
イテレータをインタフェースに利用することで、ソースコード上のクラス間依存を低減することもできる。
メインメモリ容量を超えるテキストデータをそれと意識せず操作できるバッファを「仮想バッファ」と呼ぶことにする。
┏━━━━━━━━━━━━━┓ 0..*┏━━━━━━━━━┓
┃仮想buffer<ROBuf, ABuf> ┃ ┌─→┃ Span ┃
┠─────────────┨ │ ┠─────────┨
┃Span シーケンス ┃◆─┘ ┃m_org : bool ┃ オリジナルバッファ?
┃org(RO)バッファ ┃◆──┐ ┃m_size : size_t ┃ スパンサイズ
┃追加バッファ ┃◆─┐│ ┃m_offset : size_t ┃ スパン先頭のバッファ内オフセット
┃... ┃ ││ ┠─────────┨
┃ ┃ ││ ┗━━━━━━━━━┛
┠─────────────┨ ││ 1┏━━━━━━━━━┓
┃empty() const : bool ┃ │└→┃ ROBuffer ┃ ※ ポリシーで与える
┃size() const : size_t ┃ │ ┠─────────┨
┃operator[](size_t) : uchar┃ │ ┠─────────┨
┃push_back(uchar) ┃ │ ┃operator[](size_t)┃
┃erase(size_t) ┃ │ ┃open(cchar*) ┃
┃insert(size_t, uchar) ┃ │ ┗━━━━━━━━━┛
┃replace(size_t, uchar) ┃ │ 1┏━━━━━━━━━┓
┃open(cchar*) ┃ └─→┃ AddBuffer ┃ ※ ポリシーで与える
┃write(cchar*) ┃ ┠─────────┨
┃... ┃ ┠─────────┨
┗━━━━━━━━━━━━━┛ ┃operator[](size_t)┃
┃push_back(uchar) ┃ データは末尾に*のみ*追加可能
┗━━━━━━━━━┛
総合評価として、100メガバイトのファイルオープン、検索&置換、ファイル別名保存 のタイムを計測
ファイル内容は 0x00, 0x01 ... 0xfe, 0xff, 0xfe, ... 0x01, 0x00 の三角パターンの繰り返しとする
0xff を検索し、それを 0x00 に置換し、結果を別ファイルへ書き出すものとする。
1: template<typename Buffer>
2: void benchmark_ReadReplaceWrite(cchar *className, cchar *fileName)
3: {
4: {
5: std::string fn = fileName;
6: fn += ".dump"; // 出力ファイル名
7: std::cout << "\nbenchmark replace 0xff -> 0x00\n";
8: std::cout << className << " \"" << fileName << "\":\n";
9: boost::timer tm;
10: Buffer vv;
11: if( !vv.open(fileName) )
12: return;
13: double t1 = tm.elapsed();
14: for(uint ix = 0; ix < vv.size(); ++ix) {
15: if( vv[ix] == 0xff )
16: vv.replace(ix, 0); // 0xff なら 0x00 に置換
17: }
18: double t2 = tm.elapsed();
19: vv.write(fn.c_str());
20: double t3 = tm.elapsed();
21: std::cout << "size = " << vv.size() << "\n";
22: std::cout << "read time : " << t1 << "\n";
23: std::cout << "replace time: " << t2 - t1 << "\n";
24: std::cout << "write time: " << t3 - t2 << "\n";
25: std::cout << "TOTAL: " << t3 << "\n";
26: }
27: }
総合評価補足として、空のバッファに上記のトライアングルパターンを書き込むのに要する時間も計測した。
※計測環境:ウルフデール3GHz (Bus 334MHz), 4GBMem (DDR2, Max bandwidth:400MHz), WinXP, VC9, Local SATA HDD
※HDBENCH による計測結果:Read 97,708KB/S, Write 79,875KB/S, FileCopy 4,043KB/S
| クラス | 読込 | 検索・置換 | 書出 | タイム合計 | pat生成 |
|---|---|---|---|---|---|
| gap_vector | 0.172 | 0.203 | 1.125 | 1.500 | 0.578 |
| 同上 | 0.172 | 0.203 | 1.157 | 1.532 | 0.593 |
| CBuffer | 0.0 | 1.766 | 11.578 | 13.344 | 7.235 |
| 同上 | 0.0 | 1.750 | 9.656 | 11.406 | 8.172 |
1: cuchar *data = (cuchar*)"ABCDE";
2: const int dataLength = strlen((cchar*)data);
3: double t1 = tm.elapsed();
4: for(uint ix = 0; ix < vv.size() - 3; ) {
5: if( vv[ix] == 0xfd && vv[ix + 1] == 0xfe && vv[ix + 2] == 0xff) {
6: vv.erase(ix, ix + 3);
7: vv.insert(ix, data, data + dataLength);
8: ix += dataLength;
9: } else
10: ++ix
11: }
※計測環境:クラークデール3.46GHz (Bus 133), 4GBMem (DDR3, Max bandwidth:667), Win7(x64), VC9, Local SATA HDD
※HDBENCH による計測結果:Read 105,784/S, Write 57,527/S, FileCopy 4,012/S
511バイト中の1バイトを1バイトで置換(0xff -> 0x00):
| クラス | 読込 | 検索・置換 | 書出 | タイム合計 | pat生成 |
|---|---|---|---|---|---|
| gap_vector | 0.110 | 0.175 | 0.730 | 1.015 | 0.464 |
| 同上 | 0.120 | 0.178 | 0.769 | 1.067 | 0.465 |
| CBuffer | 0.0 | 1.453 | 1.311 | 2.764 | 2.107 |
| 同上 | 0.0 | 1.448 | 1.269 | 2.717 | 2.126 |
511バイト中の3バイトを5バイトで置換(0xfd 0xfe 0xff -> 0x41 0x42 0x43 0x44 0x45):
| クラス | 読込 | 検索・置換 | 書出 | タイム合計 |
|---|---|---|---|---|
| gap_vector | 0.110 | 0.481 | 0.849 | 1.440 |
| 同上 | 0.120 | 0.494 | 0.673 | 1.325 |
| CBuffer | 0.0 | 1.338 | 1.260 | 2.598 |
| 同上 | 0.0 | 1.348 | 1.304 | 2.652 |
511バイト中の5バイトを3バイトで置換(0xfd 0xfe 0xff 0xfe 0xfd -> 0x41 0x42 0x43):
| クラス | 読込 | 検索・置換 | 書出 | タイム合計 |
|---|---|---|---|---|
| gap_vector | 0.111 | 0.276 | 0.867 | 1.254 |
| 同上 | 0.113 | 0.280 | 0.669 | 1.062 |
| CBuffer | 0.0 | 1.370 | 1.371 | 2.741 |
| 同上 | 0.0 | 1.345 | 1.297 | 2.642 |
┌──────┐ 1┌────────┐ 0..*┌───────┐
│ buffer_GV │◆────→│ CUndoMgr │◆────→│ CUndoItem │
├──────┤ ├────────┤ ├───────┤
│m_undoMgr │ │m_items │ ├───────┤
├──────┤ │m_current : int │ │doUndo(b*) = 0│
│do_insert() │ ├────────┤ │doRedo(b*) = 0│
│do_erase() │ │push_back(item*)│ └───────┘
│insert() │ │doUndo() │ │
│erase() │ │doRedo() │ │
│... │ │canUndo() : bool│ △
└──────┘ │canRedo() : bool│ │
└────────┘ │
┌─────┴─────┐
│ │
│ │
┌─────────┐ ┌─────────┐
│CGVUndoItemInsert │ │ CGVUndoItemErase │
├─────────┤ ├─────────┤
│m_first : index_t │ │m_first : index_t │
│m_last : index_t │ │m_last : index_t │
│m_buffer : v<uc>│ │m_buffer : v<uc>│
├─────────┤ ├─────────┤
│doUndo(buffer_GV*)│ │doUndo(buffer_GV*)│
│doRedo(buffer_GV*)│ │doRedo(buffer_GV*)│
└─────────┘ └─────────┘
insert(), erase() は undo/redo 対象とならないプリミティブなメソッド、
do_insert(), do_erase() は undo/redo 対象となるメソッドである。
do_insert() がコールされると、CUndoItemInsert オブジェクトが生成され、CUndoMgr::m_items キューに積まれる。
1: void CBuffer_GV::do_insert(index_t ix, value_type v)
2: {
3: ix = min(ix, size());
4: m_buffer.insert(ix, v);
5: m_undoMgr.push_back(new CUndoItemInsert(ix, v));
6: }
1: void CBBUndoItemInsert::doUndo(CBuffer_GV *bb, index_t& pos)
2: {
3: pos = m_first;
4: bb->erase(m_first, m_last);
5: }
6: void CBBUndoItemInsert::doRedo(CBuffer_GV *bb, index_t& pos)
7: {
8: pos = m_first;
9: bb->insert(m_first, &m_buffer[0], &m_buffer[0] + m_buffer.size());
10: }
11: bool CBBUndoMgr::doUndo(CBuffer_GV *bb, uint& pos)
12: {
13: if( !m_current ) return false;
14: m_items[--m_current]->doUndo(bb, pos);
15: return true;
16: }
17: bool CBBUndoMgr::doRedo(CBuffer_GV *bb, uint& pos)
18: {
19: if( m_current >= m_items.size() ) return false;
20: m_items[m_current++]->doRedo(bb, pos);
21: return true;
22: }
┌──────┐ 1┌────────┐ 0..*┌───────┐
│ buffer_PT │◆────→│ CUndoMgr │◆────→│ CUndoItem │
├──────┤ ├────────┤ ├───────┤
│m_undoMgr │ │m_items │ ├───────┤
├──────┤ │m_current : int │ │doUndo(b*) = 0│
│do_insert() │ ├────────┤ │doRedo(b*) = 0│
│do_erase() │ │push_back(item*)│ └───────┘
│insert() │ │doUndo() │ │
│erase() │ │doRedo() │ │
│... │ │canUndo() : bool│ △
└──────┘ │canRedo() : bool│ │
└────────┘ │
┌─────┴─────┐
│ │
│ │
┌──────────┐ ┌──────────┐
│CPTUndoItemInsert │ │ CPTUndoItemErase │
├──────────┤ ├──────────┤
│m_first : index_t │ │m_first : index_t │
│m_last : index_t │ │m_last : index_t │
│m_spans : v<Span> │ │m_spans : v<Span> │
├──────────┤ ├──────────┤
│doUndo(buffer_PT*) │ │doUndo(buffer_PT*) │
│doRedo(buffer_PT*) │ │doRedo(buffer_PT*) │
└──────────┘ └──────────┘
1: template<typename ROBuffer, typename AddBuffer>
2: bool CBuffer<ROBuffer, AddBuffer>::do_erase(uint first, uint last)
3: {
4: std::vector<SVBSpan> vs;
5: getSpans(vs, first, last);
6: bool rc = erase(first, last);
7: m_undoMgr.push_back(boost::shared_ptr<CPTUndoItem<ROBuffer, AddBuffer>>
8: (new CPTUndoItemErase<ROBuffer, AddBuffer>(first, last, vs)));
9: return true;
10: }
11:
12: template<typename ROBuffer, typename AddBuffer>
13: struct CPTUndoItemErase : public CPTUndoItem<ROBuffer, AddBuffer>
14: {
15: index_t m_first;
16: index_t m_last;
17: std::vector<SVBSpan> m_spans; // 削除された部分のスパン
18: public:
19: CPTUndoItemErase(index_t first, index_t last, const std::vector<SVBSpan> &vs)
20: : m_first(first), m_last(last), m_spans(vs), CPTUndoItem(UNDOITEM_TYPE_ERASE) {}
21: public:
22: void doUndo(CBuffer<ROBuffer, AddBuffer> *bb, uint &pos)
23: {
24: bb->insertSpans(pos = m_first, m_spans);
25: }
26: void doRedo(CBuffer<ROBuffer, AddBuffer> *bb, uint &pos)
27: {
28: bb->erase(pos = m_first, m_last);
29: }
30: };
511バイト中の1バイトを1バイトで置換(0xff -> 0x00):
| クラス | 読込 | 検索・置換 | 書出 | タイム合計 | バッファ破棄 |
|---|---|---|---|---|---|
| gap_vector | 0.113 | 2.907 | 0.658 | 3.678 | 18.609 |
| 同上 | 0.114 | 5.374 | 0.723 | 6.211 | 20.870 |
| 同上 | 0.111 | 3.423 | 0.665 | 4.199 | 18.115 |
| CBuffer | 0.000 | 4.189 | 1.092 | 5.281 | 18.447 |
| 同上 | 0.001 | 4.474 | 1.079 | 5.554 | 18.381 |
511バイト中の3バイトを5バイトで置換(0xfd 0xfe 0xff -> 0x41 0x42 0x43 0x44 0x45):
| クラス | 読込 | 検索・置換 | 書出 | タイム合計 |
|---|---|---|---|---|
| gap_vector | 0.109 | 2.681 | 0.748 | 3.538 |
| 同上 | 0.118 | 6.934 | 0.678 | 7.724 |
| CBuffer | 0.000 | 5.670 | 1.312 | 6.982 |
| 同上 | 0.000 | 5.658 | 1.120 | 6.778 |
511バイト中の5バイトを3バイトで置換(0xfd 0xfe 0xff 0xfe 0xfd -> 0x41 0x42 0x43):
| クラス | 読込 | 検索・置換 | 書出 | タイム合計 |
|---|---|---|---|---|
| gap_vector | 0.109 | 2.441 | 0.835 | 3.385 |
| 同上 | 0.117 | 6.426 | 0.681 | 7.224 |
| CBuffer | 0.0 | 5.484 | 1.107 | 6.591 |
| 同上 | 0.0 | 5.637 | 1.124 | 6.761 |
┌──────┐ 1┌────────┐ 0..*┌───────┐
│ buffer_GV │◆────→│ CUndoMgr │ ┌──→│ CUndoItem │
├──────┤ ├────────┤ │ ├───────┤
│m_undoMgr │ │m_items │──┘ ├───────┤
├──────┤ │m_current : int │ │doUndo(b*) = 0│
│do_insert() │ │m_pool_erase │◆───┐ │doRedo(b*) = 0│
│do_erase() │ │m_pool_insert │◆─┐ │ └───────┘
│insert() │ ├────────┤ │ │ │
│erase() │ │doUndo() │ │ └─────│───────┐
│... │ │doRedo() │ │ △ │
└──────┘ │canUndo() : bool│ │ │ │
│canRedo() : bool│ │ │ │
└────────┘ │ ┌─────┴─────┐ │
│ │ │ │
↓ │ │ ↓
┌─────────┐ ┌─────────┐
│CGVUndoItemInsert │ │ CGVUndoItemErase │
├─────────┤ ├─────────┤
│m_ix : index_t │ │m_ix : index_t │
│m_buffer : v<uc>│ │m_buffer : v<uc>│
├─────────┤ ├─────────┤
│doUndo(buffer_GV*)│ │doUndo(buffer_GV*)│
│doRedo(buffer_GV*)│ │doRedo(buffer_GV*)│
└─────────┘ └─────────┘
┌──────┐ 1┌────────┐ 0..*┌─────────┐
│ buffer_GV │◆────→│ CUndoMgr │ ┌──→│ CGVUndoItem │
├──────┤ ├────────┤ │ ├─────────┤
│m_undoMgr │ │m_items │──┘ │m_type │
├──────┤ │m_current : int │ │m_flags │
│do_insert() │ │m_pool │◆────→│m_first : index_t │
│do_erase() │ │m_heap : v<uc>│ │m_last : index_t │
│insert() │ ├────────┤ │m_hp_ix : index_t │
│erase() │ │doUndo() │ ├─────────┤
│... │ │doRedo() │ └─────────┘
└──────┘ │canUndo() : bool│
│canRedo() : bool│
└────────┘
1: bool CGVUndoMgr::doUndo(CBuffer_GV *bb, uint& pos)
2: {
3: if( !m_current ) return false;
4: const CGVUndoItem *ptr = m_items[--m_current];
5: cuchar *heap = &m_heap[ptr->m_hp_ix];
6: switch( ptr->m_type ) {
7: case UNDOITEM_TYPE_ERASE:
8: bb->insert(ptr->m_first, heap, heap + ptr->data_size());
9: break;
10: case UNDOITEM_TYPE_INSERT:
11: bb->erase(ptr->m_first, ptr->m_last);
12: break;
13: }
14: return true;
15: }
511バイト中の1バイトを1バイトで置換(0xff -> 0x00):
| クラス | 読込 | 検索・置換 | 書出 | タイム合計 | バッファ破棄 |
|---|---|---|---|---|---|
| gap_vector | 0.111 | 0.196 | 1.107 | 1.414 | 0.008 |
| 同上 | 0.113 | 0.197 | 0.679 | 0.989 | 0.008 |
| 同上 | 0.112 | 0.197 | 0.728 | 1.037 | 0.008 |
| CBuffer | 0.000 | 4.189 | 1.092 | 5.281 | 18.447 |
| 同上 | 0.001 | 4.474 | 1.079 | 5.554 | 18.381 |
┌──────┐ 1┌────────┐ 0..*┌─────────┐
│ buffer_PT │◆────→│ CUndoMgr │ ┌──→│ CPTUndoItem │
├──────┤ ├────────┤ │ ├─────────┤
│m_undoMgr │ │m_items │──┘ │m_type │
├──────┤ │m_current : int │ │m_flags │
│do_insert() │ │m_pool │◆────→│m_first : index_t │
│do_erase() │ │m_spans : v<s>│ │m_last : index_t │
│insert() │ ├────────┤ │m_hp_fst : index_t│
│erase() │ │doUndo() │ │m_hp_sz : index_t │
│... │ │doRedo() │ ├─────────┤
└──────┘ │canUndo() : bool│ └─────────┘
│canRedo() : bool│
└────────┘
1: bool doUndo(CBuffer<ROBuffer, AddBuffer> *bb, uint &pos)
2: {
3: if( !m_current ) return false;
4: const undoitem_t *ptr = m_items[--m_current];
5: SVBSpan *hp_first = &m_spans[ptr->m_hp_first];
6: SVBSpan *hp_last = hp_first + ptr->m_hp_size;
7: switch( ptr->m_type ) {
8: case UNDOITEM_TYPE_ERASE:
9: bb->insertSpans(ptr->m_first, hp_first, hp_last);
10: break;
11: case UNDOITEM_TYPE_INSERT:
12: bb->erase(ptr->m_first, ptr->m_last);
13: break;
14: }
15: return true;
16: }
511バイト中の1バイトを1バイトで置換(0xff -> 0x00):
| クラス | 読込 | 検索・置換 | 書出 | タイム合計 | バッファ破棄 |
|---|---|---|---|---|---|
| gap_vector | 0.111 | 0.196 | 1.107 | 1.414 | 0.008 |
| 同上 | 0.113 | 0.197 | 0.679 | 0.989 | 0.008 |
| 同上 | 0.112 | 0.197 | 0.728 | 1.037 | 0.008 |
| CBuffer | 0.000 | 2.812 | 1.209 | 4.021 | 0.001 |
| 同上 | 0.000 | 2.743 | 1.469 | 4.212 | 0.001 |
511バイト中の3バイトを5バイトで置換(0xfd 0xfe 0xff -> 0x41 0x42 0x43 0x44 0x45):
| クラス | 読込 | 検索・置換 | 書出 | タイム合計 |
|---|---|---|---|---|
| gap_vector | 0.114 | 0.590 | 0.994 | 1.698 |
| 同上 | 0.120 | 0.513 | 0.841 | 1.474 |
| CBuffer | 0.0 | 2.767 | 1.214 | 3.981 |
| 同上 | 0.0 | 2.791 | 1.368 | 4.177 |
| (3文字→5文字置換)*70万回 | |
| gap_vector + undo/redo | 0.18秒 |
| E v10.0体験版 | 約1秒 |
| H v8.01 | 約1.5秒(高速置換) |
| PT仮想バッファ | 3.4秒 |
| ViVi 3.05.048 :%s | 3.58秒 |
| メモ帳 | 5秒 |
| サクラエディタ Athlon 64 X2 4200+ | 56秒 |
| サクラエディタCore 2 Duo E6300 | 65秒 |
| T v0.93 | 高速置換モード無し |
| データ | undo/redo | |
|---|---|---|
| gap_vector | 1.5 * N | sizeof(UndoItem) + 挿入・削除データ |
| PT仮想バッファ | R + A + S * sizeof(Span) | sizeof(UndoItem) |
※ N:データサイズ、R:ROバッファ ビューサイズ、A:追加バッファ ビュー・追加エリアサイズ、S:スパン数
※ 前稿・本稿では R = 4096, A = 4096 + 4096 = 8192、sizeof(Span) = 8
| データ | undo/redo | |
|---|---|---|
| gap_vector | 200MB | (20+3+5) * 195,694 = 5,479,432 |
| PT仮想バッファ | 4,096 + 8,192 + 391,389 * 8 = 3,143,400 | 20 * 195,694 = 3,913,880 |