HDF5をC++で使う：(その4）作成開始時にサイズが決まらないデータの書き出し

# やりたいこと

例えば一定周期である計測機械からデータを取得していき、ある条件を満たしたところで計測をやめ、データをファイルに書き出すことを考えます。

これまでの説明[(その1)](https://qiita.com/zhidao/items/22ca97f7d2e55ece6a3f)[(その2)](https://qiita.com/zhidao/items/6594b2cf5c8eff2c5dc4)[(その3)](https://qiita.com/zhidao/items/85f6e537a57670823223)では、データスペースを作成したときに配列サイズは確定していました。上記の例では、計測終了の条件を満たすまでに、データをいくつサンプルできるかは事前に分かりません。リスト構造などでデータをメモリ上に保存しておき、最後にファイル出力してからメモリ破棄…でも構わないのですが、サンプル数が膨大になりそうな場合は不安があります。データセット自身も内部に配列を持つので、これを直接リサイズしながらデータを追加していけるならば、その方がメモリ効率は良くなります。

# 前準備

例となるデータ書き出しプログラムのmain()関数をまず示します。
```C++
int main(int argc, char *argv[])
{
  if( argc < 2 ){
    std::cerr << "input file name." << std::endl;
    return 1;
  }
  H5::Exception::dontPrint(); // make this progam silent against exceptions.
  H5::H5File file( argv[1], H5F_ACC_TRUNC );
  hsize_t dims[] = { 1, 5 }, dimsmax[] = { H5S_UNLIMITED, 5 };
  H5::DataSpace dataspace;
  dataspace.setExtentSimple( sizeof(dims) / sizeof(hsize_t), dims, dimsmax );
  H5::DSetCreatPropList prop;
  hsize_t dimschunk[] = { 1, 5 };
  prop.setChunk( sizeof(dims) / sizeof(hsize_t), dimschunk );
  H5::DataSet dataset = file.createDataSet( DATASET_NAME, H5::PredType::STD_I32LE, dataspace, prop );
  for(int i=0; i<10; i++ )
    add_dataset( dataset, i );

  dataset.close();
  file.close();
  return 0;
}
```
最初にファイルを開くところまでは良いとして、データスペースを作るところから追ってみましょう。
```C++
  hsize_t dims[] = { 1, 5 }, dimsmax[] = { H5S_UNLIMITED, 5 };
  H5::DataSpace dataspace;
  dataspace.setExtentSimple( sizeof(dims) / sizeof(hsize_t), dims, dimsmax );
```
最後のsetExtentSimple()メソッド引数は、`H5::DataSpace`のコンストラクタに与えてしまっても構わないのですが、後から行う場合に使うべきメソッドが明らかになるように敢えてこのようにしました。

dimsで、最初のデータスペースを1×5の2次元配列とすることを指定しています。
これまでと違い、配列の次元（第1引数）とサイズを決める配列（第2引数）に続いてdimsmaxという配列を与えています。これは、「最初はdimsで決まるサイズを確保するが、その後dimsmaxで決まるサイズまで拡張される可能性がある」という指示になります。例えばdimsmax[0]が10ならば、最初は1×5だがその後に最大10×5までサイズを増やせる、ということです。

そしてそのdimsmaxの要素ですが、上の例ではdimsmax[0]が`H5S_UNLIMITED`となっています。これは、「1番目のサイズは無制限に増える可能性がある」という指示になります。dimsmax[1]も同じく`H5S_UNLIMITED`ならば、2番目のサイズも無制限に増やせることになりますが、ここではそちらは5個に固定しています。

dimsmaxは常に指定できるわけではなく、データが明示的に**チャンク化**されていなければなりません。チャンク化については[本家ページの説明](https://support.hdfgroup.org/HDF5/doc/Advanced/Chunking/)をご参照下さい。大まかに言えば、データをディスク上に展開する際に順番に切り出す、最小単位がチャンクです。上記リンク先で示されているのは、9×9配列を3×3配列に分解する例です。元の配列を
```
(0,0), (0,1), (0,2), (0,3), (0,4), (0,5), (0,6), (0,7), (0,8), (1,0), (1,1), ...
```
とシリアルに展開するのではなく、アクセス効率を考えて
```
(0,0), (0,1), (0,2), (1,0), (1,1), (1,2), (2,0), (2,1), (2,2), (0,3), (0,4), ...
```
と展開するということです。

筆者もまだ理解し切れてない部分もあるのですが、プログラム例では次のようにしました。
```C++
  H5::DSetCreatPropList prop;
  hsize_t dimschunk[] = { 1, 5 };
  prop.setChunk( sizeof(dims) / sizeof(hsize_t), dimschunk );
```
`H5::DSetCreatPropList`が、チャンクを含めデータセットの諸元を与えるクラスです。チャンクサイズ（ここでは1×5）を指定する配列dimschunkをsetChunk()メソッドに与えています。

データセットは、これまでと同じ文字列、データ型、データスペースに加え、上記`H5::DSetCreatePropList`インスタンスをcreateDataSet()メソッドの第4引数に与えて作成します。
```C++
  H5::DataSet dataset = file.createDataSet( DATASET_NAME, H5::PredType::STD_I32LE, dataspace, prop );
```
データはforループを10回回して適当に作成しました。
```C++
  for(int i=0; i<10; i++ )
    add_dataset( dataset, i );
```
最後にデータセットとファイルを閉じて終了です。
```C++
  dataset.close();
  file.close();
```

# データスペースの拡張とデータ追加

データ追加する自作関数add_dataset()の中身を示します。
```C++
bool add_dataset(H5::DataSet &dataset, int offset)
{
  H5::DataSpace dataspace = dataset.getSpace();
  int ndims = dataspace.getSimpleExtentNdims();
  hsize_t dims[ndims], dimsmax[ndims];
  dataspace.getSimpleExtentDims( dims, dimsmax );

  hsize_t dimsinc[] = { 1, dims[1] };
  hsize_t dimsext[] = { dims[0] + dimsinc[0], dims[1] };
  dataset.extend( dimsext );
  hsize_t dimsoffset[] = { dims[0] - 1, 0 };
  dataspace.selectHyperslab( H5S_SELECT_SET, dimsinc, dimsoffset );

  int *data = new int [dims[1]];
  for(int i=0; i<static_cast<int>(dims[1]); i++ )
    data[i] = i + offset;
  H5::DataSpace memspace( ndims, dimsinc );
  dataset.write( data, H5::PredType::STD_I32LE, memspace, dataspace );
  delete data;
  return true;
}
```
これも順番に追っていきましょう。

データセットからデータスペース情報を取得する部分は、これまでに説明したやり方とほぼ同じです。
```C++
  H5::DataSpace dataspace = dataset.getSpace();
  int ndims = dataspace.getSimpleExtentNdims();
  hsize_t dims[ndims], dimsmax[ndims];
  dataspace.getSimpleExtentDims( dims, dimsmax );
```
dimsmaxも同時に取得していることにご注意下さい。

次にデータセットを拡張します。
```C++
  hsize_t dimsinc[] = { 1, dims[1] };
  hsize_t dimsext[] = { dims[0] + dimsinc[0], dims[1] };
  dataset.extend( dimsext );
```
二つの`hsize_t`型配列が登場していますが、
 - dimsincは**拡張する部分のサイズ**
 - dimsextは**拡張された配列全体のサイズ**

です。この例ではdimsinc[1]もdimsext[1]も元のdims[1]から変えず、1番目サイズのみ1つ足すようにしています。拡張はextend()メソッドにdimsextを与えることで行われます。

拡張した部分にデータを書き込むにあたり、selectHyperslab()メソッドを用いてデータスペースの頭出しを行います。
```C++
  hsize_t dimsoffset[] = { dims[0] - 1, 0 };
  dataspace.selectHyperslab( H5S_SELECT_SET, dimsinc, dimsoffset );
```
hyperslabというのは見慣れない単語で、日本語訳が見つからないのですが、多層に重なるデータの一層分（スラブ）を意味するようです。
dimsoffsetで指定された位置からdimsincで指定された範囲のデータスペースにアクセスする、ということを意味しています。ここでは1番目オフセットをサイズ数-1、2番目オフセットを0としています。

続くforループでdims[1]=5個分のデータを新たに適当に作成しています。
```C++
  int *data = new int [dims[1]];
  for(int i=0; i<static_cast<int>(dims[1]); i++ )
    data[i] = i + offset;
```
次がちょっとややこしいところで（筆者も理解し切ってはいません）、既に確保されたファイル上データスペースの頭出しされた位置に上記データを転送するにあたり、メモリ上の作業データスペースを定義しています。拡張する部分のサイズが1×5なので、これと同じサイズのものとします。
```C++
  H5::DataSpace memspace( ndims, dimsinc );
  dataset.write( data, H5::PredType::STD_I32LE, memspace, dataspace );
```
write()メソッドの第3引数にメモリ上データスペース、第4引数にファイル上データスペースをそれぞれ指定していることにご注意下さい。

# サンプルプログラムの全体と実行結果

main()関数と自作add_dataset()関数を含むプログラムの全体を示します。
```C++
#include <iostream>
#include <H5Cpp.h>

#define DATASET_NAME "/dataset"

bool add_dataset(H5::DataSet &dataset, int offset)
{
  H5::DataSpace dataspace = dataset.getSpace();
  int ndims = dataspace.getSimpleExtentNdims();
  hsize_t dims[ndims], dimsmax[ndims];
  dataspace.getSimpleExtentDims( dims, dimsmax );

  hsize_t dimsinc[] = { 1, dims[1] };
  hsize_t dimsext[] = { dims[0] + dimsinc[0], dims[1] };
  dataset.extend( dimsext );
  hsize_t dimsoffset[] = { dims[0] - 1, 0 };
  dataspace.selectHyperslab( H5S_SELECT_SET, dimsinc, dimsoffset );

  int *data = new int [dims[1]];
  for(int i=0; i<static_cast<int>(dims[1]); i++ )
    data[i] = i + offset;
  H5::DataSpace memspace( ndims, dimsinc );
  dataset.write( data, H5::PredType::STD_I32LE, memspace, dataspace );
  delete data;
  return true;
}

int main(int argc, char *argv[])
{
  if( argc < 2 ){
    std::cerr << "input file name." << std::endl;
    return 1;
  }
  H5::Exception::dontPrint(); // make this progam silent against exceptions.
  H5::H5File file( argv[1], H5F_ACC_TRUNC );
  hsize_t dims[] = { 1, 5 }, dimsmax[] = { H5S_UNLIMITED, 5 };
  H5::DataSpace dataspace;
  dataspace.setExtentSimple( sizeof(dims) / sizeof(hsize_t), dims, dimsmax );
  H5::DSetCreatPropList prop;
  hsize_t dimschunk[] = { 1, 5 };
  prop.setChunk( sizeof(dims) / sizeof(hsize_t), dimschunk );
  H5::DataSet dataset = file.createDataSet( DATASET_NAME, H5::PredType::STD_I32LE, dataspace, prop );
  for(int i=0; i<10; i++ )
    add_dataset( dataset, i );

  dataset.close();
  file.close();
  return 0;
}
```
これをコンパイル＆実行し作成した.h5ファイルをh5dumpで出力した結果は、次のようになります。
```
HDF5 "test.h5" {
GROUP "/" {
   DATASET "dataset" {
      DATATYPE  H5T_STD_I32LE
      DATASPACE  SIMPLE { ( 11, 5 ) / ( H5S_UNLIMITED, 5 ) }
      DATA {
      (0,0): 0, 1, 2, 3, 4,
      (1,0): 1, 2, 3, 4, 5,
      (2,0): 2, 3, 4, 5, 6,
      (3,0): 3, 4, 5, 6, 7,
      (4,0): 4, 5, 6, 7, 8,
      (5,0): 5, 6, 7, 8, 9,
      (6,0): 6, 7, 8, 9, 10,
      (7,0): 7, 8, 9, 10, 11,
      (8,0): 8, 9, 10, 11, 12,
      (9,0): 9, 10, 11, 12, 13,
      (10,0): 0, 0, 0, 0, 0
      }
   }
}
}
```
最後の1個だけ全成分が0になっていますが、これは最初の配列サイズを1×5とした名残です。データを全てadd_dataset()関数で追加しているので、最初は0×5とできれば綺麗になるのですが、仕様上サイズ0の配列を作成することが出来ないようです。これを避けるためには、最初の1個だけは別処理で作成しなければならなそうです。