数値型をUtf8に変換 – 小数点以下の話(1)

c#で、数値型をUtf8に変換します。

int i = 3;
byte[] bytes = Encoding.UTF8.GetBytes(i.ToString());

これで、Utf8のbyte[]の完成です。

このブログでは、特にシビアな速度にフォーカスを合わせてますので、これでは終われません。
まず、上記コードは動きますが、パフォーマンス的にはいろいろといただけません。
まず、i.ToString()したところで、stringのobjectを一つ作ることになります。
objectは遅いです。メモリを使用します。GC時にすごくコストが発生します。
さて次に、ToStringで作られるのは、Utf16の文字列です。
数値→Utf16→Utf8 より、数値→Utf8の方が速いですよね。なので、自作する方向で行きましょう。

さて、今回のお題は、実はただの数値ではなく、小数点以下の処理がターゲットです。
元の数値が、1234.5678とかだったりした時に、「.5678」のあたりをどう処理するのかというお話が中心です。

今回の前提として、固定で小数点を扱いますので、1234.5678という値をlongで、123456780000として持つこととします。
そして、123456780000を、”1234.5678″(Utf8)に変換します。
まず、メソッドの定義です。

public static byte[] ToUtf8(long value)

このようにシンプルに定義できます。この定義も残しますが、パフォーマンス的にはこれでは十分ではありません。
数値を文字に変換する場合、ファイルに書き出したり、APIとして外部に送信したりと、byte[]でそのまま使うことは少ないです。
実際には、長めのバッファーに対して、書き込む用途が多くなります。
バッファーに書き込む用途で、上の定義を使用すると、一度byte[]を作って、それをコピーすることになります。
使い方はこんな感じ

byte[] buffer = new byte[1024];
int offset = 0;

long value = 1_234__5678_0000;
byte[] utf8value = ToUtf8(value);
utf8value.CopyTo(buffer.AsSpan(offset));
offset += utf8value.Length;

まず、byte[]はobjectです。そう作ってはなりません。
次にコピーです。コピーもコストです。
では、どのように定義しましょうか。

public static int WriteUtf8(Span destination, long value)

この定義だと、Spanに書き込みます。
呼出元は、offsetを進めるために、何文字書き込まれたかを知る必要があります。
そのため、書き込んだ文字数を、intでreturnします。
使い方はこんな感じ。

byte[] buffer = new byte[1024];
int offset = 0;

long value = 1_234__5678_0000;
offset += WriteUtf8(buffer.AsSpan(offset), value);

bufferが書き込むのに足らなかった場合は、Exceptionが返って来ることになります。
bufferが足りる前提の時はこれで良いのですが、足りない可能性がある時はExceptionが返って来るとパフォーマンス的に死にます。
その時のために、以下の様な定義を用意します。

public static bool TryWriteUtf8(long value, Span destination, out int writtenLength)

バッファーが足りなかった時は、falseをreturnします。
使い方で、バッファーを拡張して、再度呼出を行います。
使い方はこんな感じ。

byte[] buffer = new byte[1024];
int offset = 0;

long value = 1_234__5678_0000;
while(true)
{
    if (TryWriteUtf8(value, buffer.AsSpan(offset), out var writtenLength) )
    {
        offset += writtenLength;
        break;
    }
    var newBuffer = new byte[buffer.Length * 2];
    buffer.AsSpan(0, offset).CopyTo(newBuffer);
    buffer = newBuffer;
}

さて、メソッド定義はできました。

長くなってきたので、内部実装は、また次回に。

今回のメソッド定義ですが、.netのライブラリになるべく合わせてみた感じです。
IntのToString()は、シンプルにstringを返します。
Spanでは、CopyTo, TryCopyTo があって、CopyToはバッファーが足りない時Exceptionを、TryCopyToは、returnでfalseを返します。
CopyTo/TryCopyToでは、成功時のコピーした文字数は呼出元が知っているので、返す必要はありませんが、変換系である今回のケースは書き込んだ数を返す必要があります。
bufferは、byte[]である場合と、Spanである場合がありますが、byte[]であってもSpanで受け取れるので、Spanだけ作っておけばよさそうです。
以前、ベンチマークを取ってみたこともあるのですが、byte[]からSpanに変換されても、速度は殆ど変わりませんでした。

投稿日:
カテゴリー: C#

1件のコメント

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です