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

さて、前回は仕様とメソッド定義を決めました。
仕様:
1234.5678という値をlongで、123456780000として持つ。
そして、123456780000を、”1234.5678″(Utf8)に変換するという仕様とします。
メソッド定義:

public static byte[] ToUtf8(long value)

public static int WriteUtf8(long value, Span<byte> destination)

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

今回は、実装をしていきますが、ここではTryWriteUtf8の実装を行います。
ToUtf8,WriteUtf8は、TryWriteUtf8を呼び出すか、コピーをちょっと変えると実装できますので。
今回はせっかくなので、やっていることをできるだけ全部解説していこうと思います。
なお、今回の実装の元のコードは、Utf8Jsonのコードを参考にさせてもらっています。

static uint Div1000(uint value) => value * 8389 >> 23;
static uint Div100(uint value) => value * 5243 >> 19;
static uint Div10(uint value) => value * 6554 >> 16;

まず、使用する関数を3つ定義します。
これは、valueを10,100,1000で割った結果を返します。
value / 10 などでも同じ結果になります。
掛け算とシフト演算をすることで、割り算をするより速くなります。
このDivXは、インライン展開されることを想定しています。なので、メソッド化されていても遅くはなりません。

public static bool TryWriteUtf8(long value, Span<byte> destination, out int writtenLength)
{

writtenLength = 0;

int offset = 0;
int offset2;
uint num1, num2, num3, num4, num5, num6, num7, num8, div;
ulong valueA, valueB;

メインの関数の始めに、outで使うwrittenLengthを0で初期化。
後で使う変数を定義しておきます。

if (value < 0)
{
    if (value == long.MinValue)
    {
        ReadOnlySpan<byte> minValue = "-92233720368.54775808"u8;

        writtenLength = minValue.Length;
        return minValue.TryCopyTo(destination);
    }

    if (destination.Length == 0) { return false; }
    destination[offset++] = (byte)'-';
    valueB = (ulong)(unchecked(-value));
}
else
{
    valueB = (ulong)value;
}

マイナスの場合の処理です。
マイナスの場合の’-‘を書き出し、絶対値をvalueBに入れるます。
困る点として、value == long.MinValue だった場合、* -1 をしても、絶対値が取れないんですよね。longの範囲外になってしまうので。
なので、value == long.MinValueだった場合だけ、個別処理をして終わらせてしまいます。

valueA = valueB / 1__0000_0000;

uint underPoint = (uint)(valueB - (valueA * 1__0000_0000));

ここで、小数点以上(valueA)と以下(underPoint)に分解します。

さて、次の長いコードは、小数点以上の処理

    if (valueA < 10000)
    {
        num1 = (uint)valueA;
        if (num1 < 10)
        {
            if (destination.Length < offset + 1) { return false; }
            goto L1;
        }
        if (num1 < 100)
        {
            if (destination.Length < offset + 2) { return false; }
            goto L2;
        }
        if (num1 < 1000)
        {
            if (destination.Length < offset + 3) { return false; }
            goto L3;
        }
        if (destination.Length < offset + 4) { return false; }
        goto L4;
    }
    else
    {
        valueB = valueA / 10000;
        num1 = (uint)(valueA - valueB * 10000);
        if (valueB < 10000)
        {
            num2 = (uint)valueB;
            if (num2 < 100)
            {
                if (num2 < 10)
                {
                    if (destination.Length < offset + 5) { return false; }
                    goto L5;
                }
                if (destination.Length < offset + 6) { return false; }
                goto L6;
            }
            if (num2 < 1000)
            {
                if (destination.Length < offset + 7) { return false; }
                goto L7;
            }
            if (destination.Length < offset + 8) { return false; }
            goto L8;
        }
        else
        {
            valueA = valueB / 10000;
            num2 = (uint)(valueB - valueA * 10000);
            num3 = (uint)valueA;
            if (num3 < 100)
            {
                if (num3 < 10)
                {
                    if (destination.Length < offset + 9) { return false; }
                    goto L9;
                }
                if (destination.Length < offset + 10) { return false; }
                goto L10;
            }
            if (num3 < 1000)
            {
                if (destination.Length < offset + 11) { return false; }
                goto L11;
            }
            if (destination.Length < offset + 12) { return false; }
            goto L12;

        L12:
            destination[offset++] = (byte)('0' + (div = Div1000(num3)));
            num3 -= div * 1000;
        L11:
            destination[offset++] = (byte)('0' + (div = Div100(num3)));
            num3 -= div * 100;
        L10:
            destination[offset++] = (byte)('0' + (div = Div10(num3)));
            num3 -= div * 10;
        L9:
            destination[offset++] = ((byte)('0' + (num3)));
        }
    L8:
        destination[offset++] = (byte)('0' + (div = Div1000(num2)));
        num2 -= div * 1000;
    L7:
        destination[offset++] = (byte)('0' + (div = Div100(num2)));
        num2 -= div * 100;
    L6:
        destination[offset++] = (byte)('0' + (div = Div10(num2)));
        num2 -= div * 10;
    L5:
        destination[offset++] = (byte)('0' + num2);
    }

L4:
    destination[offset++] = (byte)('0' + (div = Div1000(num1)));
    num1 -= div * 1000;
L3:
    destination[offset++] = (byte)('0' + (div = Div100(num1)));
    num1 -= div * 100;
L2:
    destination[offset++] = (byte)('0' + (div = Div10(num1)));
    num1 -= div * 10;
L1:
    destination[offset++] = (byte)('0' + num1);

小数点以上の値を、4桁毎に分解していきつつ、必要な桁数を求めます。
下から、num1,num2,num3に分解します。
分解しつつ、桁数がわかったところで、bufferのチェックをしてから、必要な書き出しの場所へジャンプします。
最後は、ジャンプされたところから、順番に書き出しをするだけです。
特記する点として、数値の現れる頻度を考慮して、4桁以下の場合は1桁→2桁→3桁・4桁の判定になっています。
分岐の回数が、1桁(1回)、2桁(2回)、3桁(3回)、4桁(3回)となります。
5~8桁の場合は、6桁以内かどうかを先に判定しています。結果、5桁(2回)、6桁(2回)、7桁(2回)、8桁(2回)となります。
5桁の場合は、ちょっと遅くなりますが、7桁・8桁の場合にはちょっと速くなります。
9~12桁は、5~8桁と同様です。
valueA,valueBは、商と余りを交互に入れていくことで、少し代入が減っています。
num1などは、4桁以下であるため、処理速度を考慮して、もとのulongではなく、uintにしてあります。

さて、次に行きましょう。次は、小数点以下の処理です。

if (underPoint > 0)
{
    num8 = underPoint;

    num8 -= (num4 = num8 / 10000) * 10000; // 実行後、num4:1~4桁目の値 num8:5~8桁目の値
    num4 -= (num2 = Div100(num4)) * 100; // 実行後、num2:1~2桁目の値 num4:3~4桁目の値
    num2 -= (num1 = Div10(num2)) * 10; // 実行後、num1:1桁目の値 num2:2桁目の値

    if (num8 > 0) // 5~8桁を評価
    {
        // 小数点以下出力は、5桁以上
        num4 -= (num3 = Div10(num4)) * 10; // 実行後、num3:3桁目の値 num4:4桁目の値
        num8 -= (num6 = Div100(num8)) * 100; // 実行後、num6:5~6桁目の値 num8:7~8桁目の値
        num6 -= (num5 = Div10(num6)) * 10; // 実行後、num5:5桁目の値 num6:6桁目の値
        if (num8 > 0) // 7~8桁を評価
        {
            // 小数点以下出力は、7or8桁
            num8 -= (num7 = Div10(num8)) * 10; // 実行後、num7:7桁目の値 num8:8桁目の値
            if (num8 > 0) // 8桁を評価
            {
                // 小数点以下出力は、8桁
                offset2 = offset += 9;
                if (destination.Length < offset) { return false; }
                goto LM8;
            }
            else
            {
                // 小数点以下出力は、7桁
                offset2 = offset += 8;
                if (destination.Length < offset) { return false; }
                goto LM7;
            }
        }
        else
        {
            // 小数点以下出力は、5or6桁
            if (num6 > 0) // 6桁を評価
            {
                // 小数点以下出力は、6桁
                offset2 = offset += 7;
                if (destination.Length < offset) { return false; }
                goto LM6;
            }
            else
            {
                // 小数点以下出力は、5桁
                offset2 = offset += 6;
                if (destination.Length < offset) { return false; }
                goto LM5;
            }
        }
    }
    else
    {
        // 小数点以下出力は、4桁以下
        if (num4 > 0) // 3~4桁を評価
        {
            // 小数点以下出力は、3or4桁
            num4 -= (num3 = Div10(num4)) * 10; // 実行後、num3:3桁目の値 num4:4桁目の値
            if (num4 > 0) // 4桁を評価
            {
                // 小数点以下出力は、4桁
                offset2 = offset += 5;
                if (destination.Length < offset) { return false; }
                goto LM4;
            }
            else
            {
                // 小数点以下出力は、3桁
                offset2 = offset += 4;
                if (destination.Length < offset) { return false; }
                goto LM3;
            }
        }
        else
        {
            // 小数点以下出力は、1or2桁
            if (num2 > 0) // 2桁を評価
            {
                // 小数点以下出力は、2桁
                offset2 = offset += 3;
                if (destination.Length < offset) { return false; }
                goto LM2;
            }
            else
            {
                // 小数点以下出力は、1桁
                offset2 = offset += 2;
                if (destination.Length < offset) { return false; }
                goto LM1;
            }
        }
    }

LM8:
    destination[--offset2] = (byte)('0' + num8);
LM7:
    destination[--offset2] = (byte)('0' + num7);
LM6:
    destination[--offset2] = (byte)('0' + num6);
LM5:
    destination[--offset2] = (byte)('0' + num5);
LM4:
    destination[--offset2] = (byte)('0' + num4);
LM3:
    destination[--offset2] = (byte)('0' + num3);
LM2:
    destination[--offset2] = (byte)('0' + num2);
LM1:
    destination[--offset2] = (byte)('0' + num1);
    destination[--offset2] = (byte)'.';
}

桁数と各桁の値を計算していきます。
小数点以下の1桁目から順番に調べていくと、最大8回のifが発生してしまうため、ちょっと複雑な方法になります。
まず、8桁の小数点以下を、4桁分/4桁分に分割します。後ろ4桁が0なら、4桁未満が確定します。
4桁分を、2桁分/2桁分、2桁分を1桁分/1桁分に分解することで、3回のifで、桁数が確定できます。
同時に、各桁の数値を計算していき、最終的に、1~8桁の値が、num1,num2,…,num8に入ります。
桁数の確定から、bufferチェックを行い、ジャンプして、必要な分を書き込みます。
後ろの部分が、0なら書き込み不要になるため、逆順に後ろから書いていきます。

これで、小数点以上・以下の両方の書き込みが終わりました。

    writtenLength = offset;
    return true;
}

最後に、書き込んだ長さをセットして、trueを返せば終わりです。

さて、ベンチマークも取ってみましょう。

static byte[] _buffer = new byte[1024];
static long _fixedPointValue = -1234__5678_0000;
static double _doubleValue = -1234.5678;
static decimal _decimalValue = -1234.5678m;

[Benchmark]
public string DoubleToString()
{
    return _doubleValue.ToString();
}

[Benchmark]
public string DecimalToString()
{
    return _decimalValue.ToString();
}

[Benchmark]
public byte[] FixedPointToByteArray()
{
    int offset = 0;
    if (WriteNumber.TryWriteUtf8(_fixedPointValue, _buffer.AsSpan(offset), out var writtenLength) == false)
    {
        throw new Exception("Buffer不足");
    }
    offset += writtenLength;

    return _buffer.AsSpan(0, offset).ToArray();
}

[Benchmark]
public (byte[], int) FixedPointReturnBuffer()
{
    int offset = 0;
    if (WriteNumber.TryWriteUtf8(_fixedPointValue, _buffer.AsSpan(offset), out var writtenLength) == false)
    {
        throw new Exception("Buffer不足");
    }
    offset += writtenLength;

    return (_buffer, offset);
}

//| Method                 | Mean      | Error    | StdDev   |
//|----------------------- |----------:|---------:|---------:|
//| DoubleToString         | 126.09 ns | 0.802 ns | 0.788 ns |
//| DecimalToString        |  59.52 ns | 0.640 ns | 0.567 ns |
//| FixedPointToByteArray  |  17.95 ns | 0.065 ns | 0.055 ns |
//| FixedPointReturnBuffer |  11.23 ns | 0.179 ns | 0.140 ns |

doubleとdecimalのToString()と比較してみました。
ToString()では、objectが一つ作られるので、FixedPointの方もToArray()しての速度比較も行いました。
きちんと、速度も出ていて安心しました。

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

1件のコメント

コメントする

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