LoginSignup
13
14

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-03-25

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

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

バイナリーの文字列変換

// #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割ほど遅くなる。

// #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掲載情報

13
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
14