バイナリーデータのシリアライズ

C#でアプリケーションを作るときに、バイナリーデータをテキスト形式で取り扱うまとめ。
重い処理はC/C++などで実装するけど、ちょっとしたときに遅いと困るので測ってみた。

使用目的の1つとして、XMLの値として使うため、Unicodeの全領域を使うことはできない（\u0000など）。また、可逆変換であることも前提。


# バイナリーの文字列変換

```csharp
// #0: 比較用 String
string conv(byte[] data) {
  // {0, 1, 2, 255} -> "\x00\x01\x02\xFF"
  return new String(Array.ConvertAll(str, x => (char)x));
}

// #1: MSDN - BitConverter を使う
string conv(byte[] data) {
  // {0, 1, 2, 255} -> "00-01-02-FF"
  return BitConverter.ToString(data);
}

// #2: StringBuilderとToString
string conv(byte[] data) {
  // {0, 1, 2, 255} -> "000102FF"
  var sb = new StringBuilder(data.Length * 2);
  foreach (byte b in data) {
    sb.Append(b.ToString("X2"));
  }
  return sb.ToString();
}

// #3: 自前で変換（通常16進）
string conv(byte[] data) {
  // {0, 1, 2, 255} -> "000102FF"
  const string hex[] = "0123456789ABCDEF";
  // unsafe/fixed を使うとかえって遅い
  var c = new char[data.Length * 2];
  for (int i = 0; i < data.Length; ++i) {
    c[i * 2    ] = hex[data[i] >> 4];
    c[i * 2 + 1] = hex[data[i] % 16];
  }
  return new String(c);
}

// #4: 自前で変換（変則16進）
string conv(byte[] data) {
  // {0, 1, 2, 255} -> "AAABACPP"
  var c = new char[data.Length * 2];
  // unsafe/fixed を使うとかえって遅い
  for (int i = 0; i < data.Length; ++i) {
    c[i * 2    ] = (char)('A' + (data[i] >> 4));
    c[i * 2 + 1] = (char)('A' + (data[i] % 16));
  }
  return new String(c);
}

// #5: Base64
string conv(byte[] data) {
  // {0, 1, 2, 255} -> "AAEC/w=="
  return Convert.ToBase64String(data);
}

```

それぞれの手法の時間を計測。
時間は、256バイトを2^20回変換したときのもの。
状況は考慮しないで、手元の環境だけで考える。

|方法|時間(msec.)|
|:-----|-----:|
|#0:String|814|
|#1:BitConverter|1086|
|#2:ToString|16888|
|#3:自前で変換|717|
|#4:自前で変換|480|
|#5:ToBase64|487|


# 文字列のバイナリー化

文字列化したバイナリーを元に戻す仕組みは、Base64を除くと用意されていないようだ。
BitConverter の逆変換も含めて、自前処理を書く。
\#2 の最適化は無用と判断した。
\#1, #3, #4 で unsafe を使わない場合は、2割ほど遅くなる。


```csharp
// #0: String
byte[] rev(string str) {
  return Array.ConvertAll(str.ToCharArray(), x => (byte)x);
}

// #1: BitConverter 出力形式を逆変換
int[] table = new int[128] {0, 0, ..., 0, 1, 2, ..., 9, ..., 10, 11, ...};  // 変換表
unsafe byte[] rev(string str) {
  byte[] b = new byte[(str.Length + 1) / 3];
  fixed (Byte* p = b)
  fixed (Char* ptr = str)
  fixed (Int32* pt = table)
  {
    for (int i = 0; i < b.Length; ++i) {
      p[i] = (byte)(tp[ptr[i * 3]] * 16 + tp[ptr[i * 3 + 1]]);
    }
  }
  return b;
}

// #2: Byte.Parse
byte[] rev(string str) {
  // "000102FF" -> {0, 1, 2, 255}
  byte[] b = new byte[str.Length / 2];
  for (int i = 0; i < str.Length; i += 2) {
    b[i / 2] = Byte.Parse(str.Substring(i, 2), System.Globalization.NumberStyles.AllowHexSpecifier);
  }
  return b;
}

// #3: 自前で変換（通常16進）
unsafe byte[] rev(string str) {
  byte[] b = new byte[str.Length / 2];
  fixed (Byte* p = b)
  fixed (Char* ptr = str)
  fixed (Int32* pt = table)
  {
    for (int i = 0; i < b.Length; ++i) {
      p[i] = (byte)(tp[ptr[i * 2]] * 16 + tp[ptr[i * 2 + 1]]);
    }
  }
  return b;
}

// #4: 自前で変換（変則16進）
unsafe byte[] rev(string str) {
  byte[] b = new byte[str.Length / 2];
  fixed (Byte* p = b)
  fixed (Char* ptr = str)
  {
    for (int i = 0; i < b.Length; ++i) {
      p[i] = (byte)((ptr[i * 2] - 'A') * 16 + ptr[i * 2 + 1] - 'A');
    }
  }
  return b;
}

// #5: Base64
byte[] rev(string str) {
  return Convert.FromBase64String(str);
}

```

結果は次の通り。

|方法|時間(msec.)|
|:-----|-----:|
|#0:String|785|
|#1:BitConverter逆変換|438|
|#2:Byte.Parse|27556|
|#3:自前で変換|423|
|#4:自前で変換|284|
|#5:FromBase64|758|


# いくつかのトピック

ループの方法

+ LINQは残念ながら遅い
+ foreachは少し遅い
+ forかwhileがベストか

変数など

+ ループ変数を2つ使うとぐっと遅い
+ 配列の添え字計算は、/ 2 より * 2 が速い

その他

+ fixedの利用は、最後の最後のだめ押し。遅くなるときもある。
+ fixedを使うなら、関連する配列などは全部まとめる。


# まとめ

+ ほとんどの場合は、Base64を使おう
+ 英数字だけを出力したいときは、単純な自前での変換を用意する。処理を簡略化するなら速い。
+ 16進表記を逆変換するのに、用意された適切な機能はないかも？


# 参考

[MSDN掲載情報](https://msdn.microsoft.com/ja-jp/library/bb311038.aspx)