3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

.NET8での配列要素に対する一部Math関数の高速化について

Last updated at Posted at 2024-10-14

概要

  • .NET8では配列の要素に対してMath.SqrtMath.Floorを適用する場合に処理がやや遅い
  • なぜか+0.0/1.0などの無駄な操作を書くと速くなる
  • 多分ビルド時orランタイムの最適化の問題だと思うが原因不明

詳細

.NET8で配列の要素をMath.SqrtとかMath.Floorするときに配列の要素を直接入れるより、値の変わらない自明な計算を挟んだ方がパフォーマンスが2倍以上よくなる場合があったのでメモとして記事にしておきます。

使用したコード

長さ1億のdoubleの配列を作成して、forループで各要素の平方根や床関数の値を計算して合計値を返すプログラム。

使用したコード
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
  </ItemGroup>

</Project>
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System;

public class MyBenchmark
{
    private double[] data;

    [GlobalSetup]
    public void Setup()
    {
        // ベンチマーク用の配列を初期化
        int N = 100_000_000;
        data = new double[N];
        Random rand = new Random(Seed: 3);
        for (int i = 0; i < data.Length; i++)
        {
            data[i] = rand.NextDouble() * 1000;
        }
    }

    [Benchmark]
    public double FloorSimple()
    {
        double sum = 0;
        for (int i = 0; i < data.Length; i++)
        {
            sum += Math.Floor(data[i]);
        }
        return sum;
    }

    [Benchmark]
    public double FloorAdd0()
    {
        double sum = 0;
        for (int i = 0; i < data.Length; i++)
        {
            sum += Math.Floor(data[i] + 0.0);
        }
        return sum;
    }

    [Benchmark]
    public double FloorMulti1()
    {
        double sum = 0;
        for (int i = 0; i < data.Length; i++)
        {
            sum += Math.Floor(data[i] * 1.0);
        }
        return sum;
    }

    [Benchmark]
    public double FloorDiv1()
    {
        double sum = 0;
        for (int i = 0; i < data.Length; i++)
        {
            sum += Math.Floor(data[i] / 1.0);
        }
        return sum;
    }

    [Benchmark]
    public double SqrtSimple()
    {
        double sum = 0;
        for (int i = 0; i < data.Length; i++)
        {
            sum += Math.Sqrt(data[i]);
        }
        return sum;
    }

    [Benchmark]
    public double SqrtAdd0()
    {
        double sum = 0;
        for (int i = 0; i < data.Length; i++)
        {
            sum += Math.Sqrt(data[i] + 0.0);
        }
        return sum;
    }

    [Benchmark]
    public double SqrtMulti1()
    {
        double sum = 0;
        for (int i = 0; i < data.Length; i++)
        {
            sum += Math.Sqrt(data[i] * 1.0);
        }
        return sum;
    }

    [Benchmark]
    public double SqrtDiv1()
    {
        double sum = 0;
        for (int i = 0; i < data.Length; i++)
        {
            sum += Math.Sqrt(data[i] / 1.0);
        }
        return sum;
    }

    public static void Main(string[] args)
    {
        var summary = BenchmarkRunner.Run<MyBenchmark>();

        // デバッグ用
        //var benchmark = new MyBenchmark();
        //benchmark.Setup();
        //Console.WriteLine(benchmark.FloorSimple());
        //Console.WriteLine(benchmark.FloorAdd0());
        //Console.WriteLine(benchmark.FloorMulti1());
        //Console.WriteLine(benchmark.FloorDiv1());
        //Console.WriteLine(benchmark.SqrtSimple());
        //Console.WriteLine(benchmark.SqrtAdd0());
        //Console.WriteLine(benchmark.SqrtMulti1());
        //Console.WriteLine(benchmark.SqrtDiv1());
    }
}

実行結果

// * Summary *

BenchmarkDotNet v0.14.0, Windows 11 (10.0.22631.4317/23H2/2023Update/SunValley3)
11th Gen Intel Core i7-11700 2.50GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.403
[Host] : .NET 8.0.10 (8.0.1024.46610), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
DefaultJob : .NET 8.0.10 (8.0.1024.46610), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI

Method Mean Error StdDev
FloorSimple 203.68 ms 3.087 ms 2.887 ms
FloorAdd0 96.73 ms 1.077 ms 0.955 ms
FloorMulti1 203.79 ms 3.827 ms 3.759 ms
FloorDiv1 105.35 ms 1.387 ms 1.297 ms
SqrtSimple 393.26 ms 1.634 ms 1.448 ms
SqrtAdd0 137.75 ms 1.338 ms 1.186 ms
SqrtMulti1 399.05 ms 6.520 ms 6.098 ms
SqrtDiv1 231.69 ms 1.699 ms 1.589 ms
  • Math.Floorの場合は+0.0/1.0が何もしない場合に比べて2倍程度速い
  • Math.Sqrtの場合は+0.0が何もしない場合に比べて2.8倍程度速い
  • Math.Sqrtの場合は/1.0が何もしない場合に比べて1.7倍程度速い
  • *1.0は効果がない

原因

分かりません。dotnetのリポジトリでもこの手の単純なパフォーマンス系のissueは結構ありそうですがあんまり調べられていません。

アセンブリ読める人とか類似の事例を知っている人がいたら教えてください。

その他

以下は適当検証なので参考程度に(真面目に記録していないので勘違いがあるかも)。

  • +1.0とかでも処理時間的には同じになりそう
    • 多分配列の要素を直接Math.Floorに入れるのがよくない
    • ただそれだと*1.0で効果がない理由が不明なので違うかも
  • .NET6向けにビルドしても同じ
  • 動的PGOを無効にしても同じ
  • Spanしても変わらない
  • Math.Floorを別の自作関数でwrapperしても同じ結果
    • ただしこの場合は.NET6では+0.0なしでも高速化する場合もあった
  • Math.Sinでは+0.0/1.0は効果がなく、むしろ悪化した
3
2
1

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
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?