Jsonのデシリアライズ関係で、
“1.2345678E+15” → 1234567800000000
といった変換を、なるべくパフォーマンス良く実行したいと思いました。
1.2345678と、+15 を抜きだし、
1.2345678 * 1000000000000000 を実行したい。
この実装を考察します。
上記の場合、double * doubleとなりますが、私の使用の都合で、調べるのは、long * long の評価とします。
(固定小数点の実装を調べていて、浮動小数点は、最初から最後まで出現させたくない)
さて、まずは結果を載せておきます。
| Method | Mean | Error | StdDev |
|------------------------ |-----------:|----------:|----------:|
| MathPowBench | 25.5353 ns | 0.3383 ns | 0.2641 ns |
| ForPower10Bench | 3.8944 ns | 0.0926 ns | 0.0773 ns |
| WhileMinusPower10Bench | 3.3517 ns | 0.0751 ns | 0.0703 ns |
| WhileMinus4Power10Bench | 1.2583 ns | 0.0561 ns | 0.0768 ns |
| SwitchStatementBench | 0.7492 ns | 0.0407 ns | 0.0399 ns |
| SwitchExpressionBench | 0.7310 ns | 0.0127 ns | 0.0106 ns |
| ArrayBench | 0.1696 ns | 0.0091 ns | 0.0076 ns |
| RosBench | 11.3009 ns | 0.2496 ns | 0.3245 ns |
結論として、最も速いのは、static long[]を事前に作っておいて、それを使う方法でした。
ベンチマークでは、123,11 → 123 * 100000000000 の速度を計測しました。
環境は、Win11,.net7.0,CPU:AMD Ryzen5-3600です。
共通のコードとして、以下を用意しました。
static long sourceValue = 123;
static int powerValue = 11;
では、一つずつ見ていこうと思います。
まずは、MathPowBench
[Benchmark]
public long MathPowBench()
{
return Power0(sourceValue, powerValue);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public long Power0(long baseNumber, int power)
{
return (long)(baseNumber * Math.Pow(10, power));
}
シンプルに、Mathにある、Powを使った実装です。
実は余談がありまして、はじめ調べていた時、Math.Powが一番速かったのです。
Power0(123,11)と書いていたのですが、どうもコンパイルかJIT最適化のどちらかわかりませんが、最適化がされていて、Math.Powが実際に実行されていな様な感じでした。
リテラルを入れるのをやめて、sourceValue,powerValueという2つの変数を入れるようにしたところ、普通に遅くなったので安心しました。
遅い理由は、まあ、doubleのdouble乗なんて、普通に考えたら大変ですもんね。
longのint乗の方が速いのは当然だと思います。
さて次は、ForPower10Benchです。
[Benchmark]
public long ForPower10Bench()
{
return Power1(sourceValue, powerValue);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public long Power1(long baseNumber, int power)
{
var value = baseNumber;
for (int i = 0; i < power; i++)
{
value *= 10;
}
return value;
}
n乗の回数分、forで回して10倍を繰り返します。
Math.Powよりはかなり速いですが、遅めです。コードはシンプルですが。
次は、WhileMinusPower10Benchです。
[Benchmark]
public long WhileMinusPower10Bench()
{
return Power2(sourceValue, powerValue);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public long Power2(long baseNumber, int power)
{
var value = baseNumber;
int i = power;
while (i > 0)
{
value *= 10;
i--;
}
return value;
}
whileで引き算で書いてみました。パフォーマンスはforとあまり変わりません。
次は、WhileMinus4Power10Benchです。
[Benchmark]
public long WhileMinus4Power10Bench()
{
return Power3(sourceValue, powerValue);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public long Power3(long baseNumber, int power)
{
var value = baseNumber;
int i = power;
while (i > 3)
{
value *= 10000;
i -= 4;
}
while (i > 0)
{
value *= 10;
i--;
}
return value;
}
n乗が大きい時、ループ回数が大きくなるのを減らす目的で、4ずつ処理するようにしました。
普通に3倍くらい速くなっていますね。効果があるようです。
次は、SwitchStatementBenchです。
[Benchmark]
public long SwitchStatementBench()
{
return Power4(sourceValue, powerValue);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public long Power4(long baseNumber, int power)
{
switch (power)
{
case 1:
return baseNumber * 10;
case 2:
return baseNumber * 100;
case 3:
return baseNumber * 1000;
case 4:
return baseNumber * 10_000;
case 5:
return baseNumber * 100_000;
case 6:
return baseNumber * 1_000_000;
case 7:
return baseNumber * 10_000_000;
case 8:
return baseNumber * 100_000_000;
case 9:
return baseNumber * 1_000_000_000;
case 10:
return baseNumber * 10_000_000_000;
case 11:
return baseNumber * 100_000_000_000;
case 12:
return baseNumber * 1_000_000_000_000;
case 13:
return baseNumber * 10_000_000_000_000;
case 14:
return baseNumber * 100_000_000_000_000;
case 15:
return baseNumber * 1_000_000_000_000_000;
case 16:
return baseNumber * 10_000_000_000_000_000;
case 17:
return baseNumber * 100_000_000_000_000_000;
case 18:
return baseNumber * 1_000_000_000_000_000_000;
default:
return -1;
}
}
swithステートメントです。
ちょっと見た目に難ありですが、かなり速いです。
どうも、連番のswitchは、内部でジャンプテーブル的に分岐してくれるようで、効率が良いみたいです。
次は、SwitchExpressionBenchです。
[Benchmark]
public long SwitchExpressionBench()
{
return Power5(sourceValue, powerValue);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public long Power5(long baseNumber, int power)
{
return baseNumber * (power switch
{
1 => 10,
2 => 100,
3 => 1000,
4 => 10_000,
5 => 100_000,
6 => 1000_000,
7 => 10_000_000,
8 => 100_000_000,
9 => 1000_000_000,
10 => 10_000_000_000,
11 => 100_000_000_000,
12 => 1000_000_000_000,
13 => 10_000_000_000_000,
14 => 100_000_000_000_000,
15 => 1000_000_000_000_000,
16 => 10_000_000_000_000_000,
17 => 100_000_000_000_000_000,
18 => 1000_000_000_000_000_000,
_ => -1
});
}
switch式に変えてみました。短くなりましたw
パフォーマンスは、switchステートメントと大差がないですね。
次は、ArrayBenchです。
[Benchmark]
public long ArrayBench()
{
return Power6(sourceValue, powerValue);
}
static long[] powerArray = new long[] {
0,
10,
100,
1_000,
10_000,
100_000,
1_000_000,
10_000_000,
100_000_000,
1_000_000_000,
10_000_000_000,
100_000_000_000,
1_000_000_000_000,
10_000_000_000_000,
100_000_000_000_000,
1_000_000_000_000_000,
10_000_000_000_000_000,
100_000_000_000_000_0000,
1_000_000_000_000_000_000
};
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public long Power6(long baseNumber, int power)
{
return baseNumber * powerArray[power];
}
今回の最速方法です。
ちょっと、開始時にメモリにlong[]を作ることになりますが、それを除けば最速です。
今までの最速のswitchより4倍以上速いので、これが良いと思います。
調査に、Utf8JsonのReadDoubleの実装も読んだのですが、上記と似たdouble[]を用意する実装になっていました。
最後に、RosBenchです。
[Benchmark]
public long RosBench()
{
return Power7(sourceValue, powerValue);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public long Power7(long baseNumber, int power)
{
ReadOnlySpan<long> powerSpan = new long[] {
0,
10,
100,
1_000,
10_000,
100_000,
1_000_000,
10_000_000,
100_000_000,
1_000_000_000,
10_000_000_000,
100_000_000_000,
1_000_000_000_000,
10_000_000_000_000,
100_000_000_000_000,
1_000_000_000_000_000,
10_000_000_000_000_000,
100_000_000_000_000_0000,
1_000_000_000_000_000_000
};
return baseNumber * powerSpan[power];
}
Arrayを事前に作るのではなく、ReadOnlySpanを実行時に作るようにしました。
ReadOnlySpan<byte>だと、このような実装で最適化がかかって、すごく速いのですが、ReadOnlySpan<int>等では最適化がかからず、実行時に普通にコピーが走るようです。
そのため、Math.Powよりはましなものの、すごく遅い結果となりました。
どうも、.net8.0ではこれが最速になるかもしれないようです。期待したいですね。
今回はこんなところです。ではまた。