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進表記を逆変換するのに、用意された適切な機能はないかも?