技術文章>テキストエディタ実装技術その2
テキストエディタ ViVi ver 3.05/06 に実装予定のバイナリエディタ用仮想バッファの インタフェース・データ構造・アルゴリズムについて解説し、 VC9 を使って実装したものでパフォーマンス計測した結果を報告する。
目次:
                   ┏━━━━━━━━━━━━━━━━━━━┓
                   ┃        random_access_iterator        ┃
                   ┗━━━━━━━━━━━━━━━━━━━┛
                                     │
                                     △
                                     │
                   ┏━━━━━━━━━━━━━━━━━━━┓
             ←──┃          buffer::cursor              ┃──→
      operator--() ┠───────────────────┨ operator++()
                   ┠───────────────────┨
                   ┃operator*() const : uchar             ┃
                   ┃operator*() : uchar&                  ┃    //  undo 非対象
                   ┃operator==(const cursor&) const : bool┃
                   ┃operator>(const cursor&) const : bool ┃
                   ┃operator+=(int) : cursor&             ┃
                   ┃operator-(const cursor&) const : int  ┃
                   ┃.....                                 ┃
                   ┗━━━━━━━━━━━━━━━━━━━┛
                                     │
                                     ↓
    ┌───────────────────────────────────┐
    │                            text sequences                            │
    └───────────────────────────────────┘
     ↑
     │            ┏━━━━━━━━━━━━━━━━━┓
     │            ┃            buffer                ┃
     │            ┠─────────────────┨
     └─────◆┃                                  ┃
                   ┠─────────────────┨
                   ┃empty() const : bool              ┃
                   ┃size() const : size_t             ┃
                   ┃resize(size_t)  : void            ┃    //  undo 非対象
                   ┃operator[](uint) const : uchar    ┃
                   ┃operator[](uint) : uchar&         ┃    //  undo 非対象
                   ┃begin() : cursor                  ┃
                   ┃end() : cursor                    ┃
                   ┃open(cchar*) : bool               ┃
                   ┃write() : bool                    ┃
                   ┃insert(cursor, uchar) : cursor    ┃    //  undo 非対象
                   ┃erase(cursor, cursor) : cursor    ┃    //  undo 非対象
                   ┃replace(cursor, uchar) : cursor   ┃    //  undo 非対象
                   ┃insert(uint, uchar) : uint        ┃    //  undo 非対象
                   ┃erase(uint, cursor) : uint        ┃    //  undo 非対象
                   ┃replace(uint, uchar) : uint       ┃    //  undo 非対象
                   ┃                                  ┃
                   ┃doResize(size_t)  : void          ┃    //  undo 対象
                   ┃doInsert(uint, uchar) : uint      ┃    //  undo 対象
                   ┃doErase(uint, cursor) : uint      ┃    //  undo 対象
                   ┃doReplace(uint, uchar) : uint     ┃    //  undo 対象
                   ┃isUndoAvailable() const : bool    ┃
                   ┃isRedoAvailable() const : bool    ┃
                   ┃undo()                            ┃
                   ┃redo()                            ┃
                   ┃.....                             ┃
                   ┗━━━━━━━━━━━━━━━━━┛
            ↓ビュー
            ┏━━┓
    ┌───╂──╂────────────────────────────┐
    │      ┃    ┃              text sequences                            │
    └───╂──╂────────────────────────────┘
            ┗━━┛
  1: uchar CROBuffer::operator[](uint offset)
  2: {
  3:     if( m_hFile == INVALID_HANDLE_VALUE || offset >= m_size )
  4:         return 0;                       //  エラーの場合
  5:     if( !isViewAvailable(offset) &&     //  キャッシュバッファ内にデータがあるか?
  6:         !makeViewBuffer(offset) )       //  キャッシュバッファに読み込む
  7:     {
  8:         return 0;
  9:     }
 10:     return m_viewBuffer[offset - m_viewOffset];
 11: }
    ┏━━━━━━━━━━━━━━━━┓
    ┃        CROBuffer_Rd            ┃
    ┠────────────────┨
    ┃m_size : size_t                 ┃    //  ファイルサイズ
    ┃m_hFile : HANDLE                ┃    //  ファイルハンドル
    ┃m_viewOffset : uint             ┃    //  キャッシュバッファ先頭のファイル内オフセット
    ┃m_viewBuffer[VIEW_SIZE] : uchar ┃    //  キャッシュバッファ
    ┠────────────────┨
    ┗━━━━━━━━━━━━━━━━┛
    ┏━━━━━━━━━━━━━━━━┓
    ┃        CROBuffer_MMF           ┃
    ┠────────────────┨
    ┃m_size : size_t                 ┃    //  ファイルサイズ
    ┃m_hFile : HANDLE                ┃    //  ファイルハンドル
    ┃m_hFileMap : HANDLE             ┃    //  メモリマップトファイルハンドル
    ┃m_viewOffset : uint             ┃    //  キャッシュバッファ先頭のファイル内オフセット
    ┃m_mapView : uchar*              ┃    //  ビュー先頭へのポインタ
    ┃m_mapViewEnd : uchar*           ┃    //  ビュー末尾へのポインタ
    ┠────────────────┨
    ┗━━━━━━━━━━━━━━━━┛
  1: template<typename Buffer>
  2: void benchmark(cchar *className, cchar *fileName)
  3: {
  4:     std::cout << "\n" << className << " \"" << fileName << "\":\n";
  5:     Buffer vb;
  6:     if( !vb.open(fileName) )
  7:         return;
  8:     std::cout << "seq-access:\n";
  9:     uint sum = 0;
 10:     boost::timer tm;
 11:     for(uint ix = 0; ix < vb.size(); ++ix)
 12:         sum += vb[ix];
 13:     double t = tm.elapsed();
 14:     std::cout << "sum = " << sum << "\n";
 15:     std::cout << "time: " << t << " (" << vb.size()/t/1000000 << " MB/sec)\n";
 16: }
リードオンリー仮想バッファの 100MB, 1.5GB, 3.2GB ファイルシーケンシャルアクセス時間: 結果
シーケンシャルアクセス速度(MB/S)は以下の通り:
| 100MB | 1.5GB | 3.2GB | ||||
|---|---|---|---|---|---|---|
| gap_vector<uchar> | 1085.110 | 未計測 | 未計測 | |||
| getc | 14.192 | 14.004 | 未計測 | |||
| MMF一括読込後 | 1645.160 | 1416.970 | 読込不可 | |||
| CROBuffer_Rd 初回 | 103.553 | 78.5866 | 99.3717 | |||
| 2回目 | 384.9060 | 375.951 | 74.1967 | |||
| CROBuffer_MMF 初回 | 69.435 | 67.9337 | 69.3118 | |||
| 2回目 | 261.538 | 265.65 | 68.3198 | |||
                        ┏━━━━━━━━━━━━━━━━━┓
                        ┃         CAbstractROBuffer        ┃
                        ┠─────────────────┨
                        ┠─────────────────┨
                        ┃empty() const = 0 : bool          ┃
                        ┃size() const = 0 : size_t         ┃
                        ┃operator[](uint) const = 0 : uchar┃
                        ┃open(cchar*) = 0 : bool           ┃
                        ┃.....                             ┃
                        ┗━━━━━━━━━━━━━━━━━┛
                                          │
                                          △
                                          │
                      ┌─────────┴─────────┐
                      │                                      │
      ┏━━━━━━━━━━━━━━━┓      ┏━━━━━━━━━━━━━━━┓ 
      ┃       CDrvdROBuffer_MMF      ┃      ┃       CDrvdROBuffer_Rd       ┃ 
      ┠───────────────┨      ┠───────────────┨ 
      ┠───────────────┨      ┠───────────────┨ 
      ┃empty() const : bool          ┃      ┃empty() const : bool          ┃ 
      ┃size() const : size_t         ┃      ┃size() const : size_t         ┃ 
      ┃operator[](uint) const : uchar┃      ┃operator[](uint) const : uchar┃ 
      ┃open(cchar*) : bool           ┃      ┃open(cchar*) : bool           ┃ 
      ┃.....                         ┃      ┃.....                         ┃ 
      ┗━━━━━━━━━━━━━━━┛      ┗━━━━━━━━━━━━━━━┛ 
| 100MB | 1.5GB | 3.2GB | ||||
|---|---|---|---|---|---|---|
| CROBuffer_Rd 初回 | 103.553 | 78.5866 | 99.3717 | |||
| 2回目 | 384.9060 | 375.951 | 未計測 | |||
| CDrvdROBuffer_Rd 初回  | 
	93.2358 | 108.141 | 106.76 | |||
| 2回目 | 197.674 | 199.489 | 未計測 | |||
キャッシュが効かない場合の速度はほとんど変わらないが、キャッシュが効いていると半分程度の速度になる。
これはHDDの転送速度が律速の場合は仮想関数コールのオーバヘッドは効いてこないが、
HDDキャッシュが効いている場合はオーバヘッドを無視できなくなるということである。
従って、一定サイズ以下の場合はMMFに一括読み込みを行うようにし、派生クラスを動的に切り替えるようにしても、
仮想関数コールのオーバヘッドでそれほど高速にはならないと考えられる。
     buffer
    ┌────────┬─┬───────┬───────┐
    │                │  │              │              │ //  Span シーケンス
    └────────┴─┴───────┴───────┘
             │         │        │              │
             │         └────┼──────┐└───┐
             ↓                   ↓            ↓        ↓
    ┌────────┬─┬───────┐  ┌─┬───────┐
    │                │  │              │  │  │              │
    └────────┴─┴───────┘  └─┴───────┘
     Original Buffer                           Add Buffer
    ┏━━━━━━━━┓
    ┃      Span      ┃
    ┠────────┨
    ┃m_org : bool    ┃
    ┃m_size : uint   ┃
    ┃m_offset : uint ┃
    ┠────────┨
    ┗━━━━━━━━┛
  1: //  buffer[ix]──→ Span、span が指すバッファ内オフセット
  2: uchar buffer::operator[](uint ix) const
  3: {
  4:     if( ix が範囲外 ) return 0;
  5:     uint offset;    //  オリジナル/追加バッファ中のオフセット
  6:     const Span *span = findSpan(ix, offset);    //  ix に対応するSpanを探す。
  7:     if( span->isOriginalBuffer() )
  8:         return オリジナルバッファから offset 位置のデータを取得;
  9:     else
 10:         return 追加バッファから offset 位置のデータを取得;
 11: }
バッファ内インデックスから対応するスパンを検索する処理時間は、スパンを格納するデータ構造に依存する。
ベクター類を用いた場合、スパン数を N とすれば、処理時間は O(N) になる。
しかし、テキストエディタでは参照位置は局所化されており、直前にアクセスされた位置をキャッシュしておけば、
処理時間はほぼ O(1) となる。
                   ┏━━━━━━━━━┓     0..* ┏━━━━━━━━┓
                   ┃      buffer      ┃    ┌─→┃      Span      ┃
                   ┠─────────┨    │    ┠────────┨
     ┌─────◆┃ファイル情報      ┃    │    ┃m_size : uint   ┃  スパンサイズ
     │        ┌─┃ビュー情報        ┃    │    ┃m_org : bool    ┃  オリジナルバッファ?
     │        │  ┃Span シーケンス   ┃◆─┘    ┃m_offset : uint ┃  スパン先頭のバッファ内オフセット
     │        │  ┃ビュー情報        ┃────┐┃                ┃
     │        │  ┃追加バッファ      ┃◆─┐  │┠────────┨
     │        │  ┠─────────┨    │  │┃                ┃
     │        │  ┃                  ┃    │  │┃                ┃
     │        │  ┗━━━━━━━━━┛    │  │┗━━━━━━━━┛
     │        ↓                            │  ↓
     ↓     ┏━━━┓                       ↓ ┏━━━┓
    ┌───╂───╂──────────┐┌─╂───╂─────────┐
    │      ┃      ┃  Original Buffer   ││  ┃      ┃    Add Buffer    │
    └───╂───╂──────────┘└─╂───╂─────────┘
            ┗━━━┛                          ┗━━━┛
オリジナルバッファの部分は、先に実装した ROBuffer をそのまま利用すべきである。
そのためには継承または委譲を使用する。
        ┏━━━━━━━━━┓
        ┃     ROBuffer     ┃
        ┠─────────┨
        ┠─────────┨
        ┗━━━━━━━━━┛
                 │
                 △
                 │is-a
        ┏━━━━━━━━━┓     0..* ┏━━━━━━━━┓
        ┃      buffer      ┃    ┌─→┃      Span      ┃
        ┠─────────┨    │    ┠────────┨
        ┃Span シーケンス   ┃◆─┘    ┃m_size : uint   ┃  スパンサイズ
        ┃ビュー情報        ┃────┐┃m_org : bool    ┃  オリジナルバッファ?
        ┃追加バッファ      ┃◆─┐  │┃m_offset : uint ┃  スパン先頭のバッファ内オフセット
        ┠─────────┨    │  │┠────────┨
        ┃                  ┃    │  │┃                ┃
        ┗━━━━━━━━━┛    │  │┗━━━━━━━━┛
                                  │  ↓
                                  ↓ ┏━━━┓
                                 ┌─╂───╂─────────┐
                                 │  ┃      ┃    Add Buffer    │
                                 └─╂───╂─────────┘
                                     ┗━━━┛
                                        ┏━━━━━━━━┓
                                ┌──→┃    ROBuffer    ┃
                                │has-a ┠────────┨
        ┏━━━━━━━━━┓  │      ┠────────┨
        ┃      buffer      ┃  │      ┗━━━━━━━━┛
        ┠─────────┨  │
        ┃orgバッファ       ┃◆┘  0..*┏━━━━━━━━┓
        ┃Span シーケンス   ┃◆───→┃      Span      ┃
        ┃ビュー情報        ┃────┐┠────────┨
        ┃追加バッファ      ┃◆─┐  │┃m_size : uint   ┃  スパンサイズ
        ┠─────────┨    │  │┃m_org : bool    ┃  オリジナルバッファ?
        ┃                  ┃    │  │┃m_offset : uint ┃  スパン先頭のバッファ内オフセット
        ┗━━━━━━━━━┛    │  │┠────────┨
                                  │  │┃                ┃
                                  │  │┗━━━━━━━━┛
                                  │  ↓
                                  ↓ ┏━━━┓
                                 ┌─╂───╂─────────┐
                                 │  ┃      ┃    Add Buffer    │
                                 └─╂───╂─────────┘
                                     ┗━━━┛
追加バッファの部分も、機能が ROBuffer を拡張したものになっているので、クラス化して利用すべきである。
よって、オリジナルバッファ・追加バッファをそれぞれクラス化し、
編集可能バッファからはそれらを委譲で利用するようにした。
また、それらのクラスはポリシーとして編集可能バッファに渡すようにした。
        ┏━━━━━━━━━┓          ┏━━━━━━━━┓
        ┃      buffer      ┃    ┌─→┃    ROBuffer    ┃
        ┠─────────┨    │    ┠────────┨
        ┃orgバッファ       ┃◆─┘    ┠────────┨
        ┃                  ┃          ┗━━━━━━━━┛
        ┃Span シーケンス   ┃◆─┐0..*┏━━━━━━━━┓
        ┃                  ┃    └─→┃      Span      ┃
        ┃追加バッファ      ┃◆─┐    ┠────────┨
        ┠─────────┨    │    ┃m_size : uint   ┃  スパンサイズ
        ┃                  ┃    │    ┃m_org : bool    ┃  オリジナルバッファ?
        ┗━━━━━━━━━┛    │    ┃m_offset : uint ┃  スパン先頭のバッファ内オフセット
                                  │    ┠────────┨
                                  │    ┃                ┃
                                  │    ┗━━━━━━━━┛
                                  │    ┏━━━━━━━━┓
                                  └─→┃   AddBuffer    ┃
                                        ┠────────┨
                                        ┠────────┨
                                        ┃size()          ┃
                                        ┃empty()         ┃
                                        ┃operator[](uint)┃  //  const 
                                        ┃push_back(uchar)┃
                                        ┃.....           ┃
                                        ┗━━━━━━━━┛
  1: template<typename ROBuffer, typename AddBuffer>
  2: class CBuffer
  3: {
  4: protected:
  5:     uint    m_size;             //  バッファサイズ [0, 0xffffffff)
  6:     std::gap_vector<SVBSpan>    m_spans;        //  スパンベクター
  7: 
  8:     ROBuffer    m_orgBuffer;    //  オリジナルバッファ
  9:     AddBuffer   m_addBuffer;    //  追加ベクター
 10: public:
 11:     CBuffer() : m_size(0), m_spanCacheIX(0), m_spanCacheOffset(0) {}
 12:     ~CBuffer() { close(); }
 13: 
 14:     uchar   operator[](uint) const;
 15:     .....
 16: };
 17: template<typename ROBuffer, typename AddBuffer>
 18: uchar CBuffer<ROBuffer, AddBuffer>::operator[](uint ix)
 19: {
 20:     if( ix >= m_size ) return 0;
 21:     uint offset;    //  オリジナル or 追加バッファ内オフセット
 22:     const SVBSpan *span = getSpan(ix, offset);
 23:     if( span == NULL ) return 0;
 24:     if( span->isOrgBuffer() )
 25:         return m_orgBuffer[offset];
 26:     else
 27:         return m_addBuffer[offset];
 28: }
スワップアウトを行わない追加バッファは std::vector 処理速度(MB/sec)の計測結果を一覧にしたものを下表に示す。
 
 
  1: class CAddBuffer_vect
  2: {
  3:     std::vector<uchar>  m_buffer;
  4: public:
  5:     CAddBuffer_vect() {}
  6:     ~CAddBuffer_vect() {}
  7: 
  8: public:
  9:     size_t  size() const { return m_buffer.size(); }
 10:     bool    empty() const { return m_buffer.empty(); }
 11:     uchar operator[](uint ix) const { return m_buffer[ix]; }
 12: 
 13: public:
 14:     void    push_back(uchar x) { m_buffer.push_back(x); }
 15: };
	 100MB 
	1.5GB 
	3.2GB CROBuffer_Rd 初回 
	103.553 ![]()
78.5866 ![]()
99.3717 ![]()
2回目 
	384.9060 ![]()
375.951 ![]()
未計測 CROBuffer_MMF 初回 
	69.435 ![]()
67.9337 ![]()
69.3118 ![]()
2回目 
	261.538 ![]()
265.65 ![]()
未計測 CBuffer SO無 初回 
	69.435 ![]()
62.9477 ![]()
68.9854 ![]()
2回目 
	69.435 ![]()
68.8927 ![]()
70.4056 ![]()
※計測環境:ウルフデール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
	
     buffer           ↓erace()
    ┏━━━━━━━━━━━━━━━━━━┓
    ┃                                    ┃
    ┗━━━━━━━━━━━━━━━━━━┛
             │                 
             │                     
             ↓                     
    ┌──────────────────┐
    │                                    │
    └──────────────────┘
                      
                      ↓
     buffer           
    ┏━━━━━━━━┯━━━━━━━┓
    ┃                │              ┃
    ┗━━━━━━━━┷━━━━━━━┛
             │               │
             │               └─┐
             ↓                   ↓
    ┌────────┬─┬───────┐
    │                │  │              │
    └────────┴─┴───────┘
     Original Buffer
     buffer           ↓insert()
    ┏━━━━━━━━━━━━━━━━━━┓
    ┃                                    ┃
    ┗━━━━━━━━━━━━━━━━━━┛
             │                 
             │                     
             ↓                     
    ┌──────────────────┐
    │                                    │
    └──────────────────┘
                      
                      ↓
     buffer           
    ┏━━━━━━━━┯━┯━━━━━━━━━┓
    ┃                │  │                  ┃
    ┗━━━━━━━━┷━┷━━━━━━━━━┛
             │         │        │
             │         └────┼──────┐
             ↓                   ↓            ↓
    ┌────────┬─────────┐  ┌─┬───────┐
    │                │                  │  │  │              │
    └────────┴─────────┘  └─┴───────┘
     Original Buffer                           Add Buffer
     buffer             ↓replace()
    ┏━━━━━━━━━━━━━━━━━━┓
    ┃                                    ┃
    ┗━━━━━━━━━━━━━━━━━━┛
             │                 
             │                     
             ↓                     
    ┌──────────────────┐
    │                                    │
    └──────────────────┘
                      
                      ↓
     buffer           
    ┏━━━━━━━━┯━┯━━━━━━━┓
    ┃                │  │              ┃
    ┗━━━━━━━━┷┯┷━━━━━━━┛
             │         │        │
             │         └────┼──────┐
             ↓                   ↓            ↓
    ┌────────┬─┬───────┐  ┌─┬───────┐
    │                │  │              │  │  │              │
    └────────┴─┴───────┘  └─┴───────┘
     Original Buffer                           Add Buffer
比較のために gap_vector も同様の計測を行った。
結果
クラス 削除 挿入 置換 置換後連続参照 CBuffer_GV 0.078 (12.9474 MB/sec) 0.296 (3.48076 MB/sec) 
	0.032 (31.875 MB/sec) 0.11 (927.273 MB/sec) CBuffer SO無 0.047 (21.4873 MB/sec) 0.125 (8.24243 MB/sec) 
	0.109 (9.3578 MB/sec) 1.531 (66.6231 MB/sec) 
	
		→ スパンのフラグメント化による影響よりも、スパン経由でバッファにアクセスする処理が重いと考えられる
	
           読出用block                   書込用block(オンメモリ)
                ↓                              ↓
    ┌───┳━━━┳───┬───┬───┳━━━┓
    │      ┃      ┃      │      │      ┃      ┃
    └───┻━━━┻───┴───┴───┻━━━┛
                                            ↑
                                      m_memBufferOffset
    ┏━━━━━━━━━━━━━━━━┓
    ┃        CAddBuffer_tf           ┃
    ┠────────────────┨
    ┃m_size : size_t                 ┃    //  ファイルサイズ
    ┃m_memBufferOffset : uint        ┃    //   オンメモリバッファ 先頭オフセット
    ┃m_memBuffer : vect<uchar>     ┃    //   オンメモリバッファ
    ┃m_hFile : HANDLE                ┃    //  ファイルハンドル
    ┃m_viewOffset : uint             ┃    //  キャッシュバッファ先頭のファイル内オフセット
    ┃m_viewBuffer[VIEW_SIZE] : uchar ┃    //  キャッシュバッファ
    ┠────────────────┨
    ┗━━━━━━━━━━━━━━━━┛
  1: uchar CAddBuffer_tf::operator[](uint ix) const
  2: {
  3:     if( ix >= m_size ) return 0;
  4:     if( ix >= m_memBufferOffset )   //  オンメモリの書込用ブロックにデータがあれば読み出し
  5:         return m_memBuffer[ix - m_memBufferOffset];
  6:     if( !isViewAvailable(ix) &&     //  読出用ブロックにデータが無ければ
  7:         !makeViewBuffer(ix) )       //  読出用ブロックを移動(ビューにデータ転送)
  8:     {
  9:         return 0;
 10:     }
 11:     return m_viewBuffer[ix - m_viewOffset];     //  読出用ブロックのデータを返す
 12: }
  1: void CAddBuffer_tf::push_back(uchar v)
  2: {
  3:     if( 書込ブロックが満杯 ) {
  4:         書込みブロックをテンポラリファイルに追加;
  5:         m_memBufferOffset += SIZE_ON_MEMORY;
  6:         m_memBuffer.clear();
  7:     }
  8:     m_memBuffer.push_back(v);
  9:     ++m_size;
 10: }
処理時間の計測結果を下表に示す。
| クラス | 削除 | 挿入 | 置換 | 置換後連続参照 | 
|---|---|---|---|---|
| CBuffer_GV | 0.078 (12.9474 MB/sec) | 0.296 (3.48076 MB/sec) | 0.032 (31.875 MB/sec) | 0.11 (927.273 MB/sec) | 
| CBuffer SO無 | 0.047 (21.4873 MB/sec) | 0.125 (8.24243 MB/sec) | 0.109 (9.3578 MB/sec) | 1.531 (66.6231 MB/sec) | 
| CBuffer SO有 | 0.047 (21.4873 MB/sec) | 0.109 (9.45233 MB/sec) | 0.109 (9.3578 MB/sec) | 1.531 (66.6231 MB/sec) | 
| クラス | 読込 | 検索・置換 | 書出 | タイム合計 | 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 | 
まとめ:
今後の課題・予定:
余談:
ご清聴、ありがとうございました。m(_ _)m