🙆‍♀️

C# のローカル関数は果たして早いのか

2022/10/18に公開

Qiitaより転載
2018/4/27 初投稿


C# 7 になってからローカル関数が導入されて、場合によってはラムダ式より早いらしいという話を聞きました。
最近ベンチマークおじさんをやっているので、早速ベンチマークしてみたいと思います。

.NET Core 2.1 で String.Split は早くなっているか?

これ、Span<T> がそもそも実装されてるんだからそれ使えばいいじゃん、と思ったので、それを使ってベンチマークします。

テスト内容

前回と同じく "hoge,fuga,moge" という文字列から "hoge""moge" を取得します。

テストコード

/* using 省略*/

namespace SpanTest
{
    class Program
    {
        static void Main(string[] args)
        {
            BenchmarkRunner.Run<Test>(new BenchmarkConfig());
        }
    }

    public class Test
    {
        private readonly string Str = "hoge,fuga,moge";

        [Benchmark]
        public (string, string) UsingSplit()
        {
            /* 前回と同じなので省略 */
        }

        [Benchmark]
        public (string, string) UsingSpanSplit()
        {
            /* 前回と同じなので省略 */
        }

        [Benchmark]
        public (string, string) UseFastSpanWithLocalMethod()
        {
            ReadOnlySpan<char> span = Str.AsSpan();

            char separator = ',';
            int offset = 0;

            ReadOnlySpan<char> GetNextValue(ref ReadOnlySpan<char> strSpan)
            {
                int length = 0;
                int startPos = offset;
                for (int i = offset; i < strSpan.Length; i++)
                {
                    length++;
                    if (strSpan[i] == separator)
                    {
                        offset = i + 1;
                        break;
                    }
                }
                return strSpan.Slice(startPos, length);
            }

            var a = new string(GetNextValue(ref span));
            GetNextValue(ref span);
            var b = new string(GetNextValue(ref span));

            return (a, b);
        }

        [Benchmark]
        public (string, string) UseFastSpan()
        {
            ReadOnlySpan<char> span = Str.AsSpan();

            char separator = ',';

            int offset = 0;

            var a = new string(GetNextValue(ref span, separator, ref offset));
            GetNextValue(ref span, separator, ref offset);
            var b = new string(GetNextValue(ref span, separator, ref offset));

            return (a, b);
        }

        private ReadOnlySpan<char> GetNextValue(ref ReadOnlySpan<char> strSpan, char separator, ref int offset)
        {
            int length = 0;
            int startPos = offset;
            for (int i = offset; i < strSpan.Length; i++)
            {
                length++;
                if (strSpan[i] == separator)
                {
                    offset = i + 1;
                    break;
                }
            }
            return strSpan.Slice(startPos, length);
        }
    }


    public readonly struct MySpan
    {
        /* 前回と同じなので省略 */
    }

    public class BenchmarkConfig : ManualConfig
    {
        public BenchmarkConfig()
        {
            Add(SetRun(Job.Default.UnfreezeCopy()));

            Add(DefaultColumnProviders.Instance);
            Add(MarkdownExporter.GitHub);
            Add(new ConsoleLogger());
            Add(new HtmlExporter());
            Add(MemoryDiagnoser.Default);
        }

        private static Job SetRun(Job job)
        {
            job.Run.UnrollFactor = 5;
            job.Run.InvocationCount = 5;
            job.Run.WarmupCount = 1;
            job.Run.TargetCount = 1;
            job.Run.LaunchCount = 30;
            return job;
        }
    }

}

Span<T> を使う際、前回のオレオレSpan と同じ方法でローカル関数を使った方法と、ref でプライベート関数に必要な値を渡す方法とで、比較してみました。

結果

BenchmarkDotNet=v0.10.14, OS=Windows 10.0.16299.371 (1709/FallCreatorsUpdate/Redstone3)
Intel Core i5-5200U CPU 2.20GHz (Broadwell), 1 CPU, 4 logical and 2 physical cores
Frequency=2143478 Hz, Resolution=466.5315 ns, Timer=TSC
.NET Core SDK=2.1.300-preview2-008533
  [Host]     : .NET Core 2.1.0-preview2-26406-04 (CoreCLR 4.6.26406.07, CoreFX 4.6.26406.04), 64bit RyuJIT
  Job-SFFTDC : .NET Core 2.1.0-preview2-26406-04 (CoreCLR 4.6.26406.07, CoreFX 4.6.26406.04), 64bit RyuJIT

InvocationCount=5  LaunchCount=30  TargetCount=1
UnrollFactor=5  WarmupCount=1
                 Method |       Mean |    Error |   StdDev | Allocated |

--------------------------- |-----------:|---------:|---------:|----------:|
UsingSplit | 1,063.6 ns | 374.8 ns | 561.0 ns | 0 B |
UsingSpanSplit | 765.6 ns | 135.8 ns | 203.2 ns | 0 B |
UseFastSpanWithLocalMethod | 761.2 ns | 268.8 ns | 402.3 ns | 0 B |
UseFastSpan | 688.5 ns | 136.4 ns | 204.1 ns | 0 B |

誤差範囲内ですかね。一応ローカル関数を使っても処理が遅くなることはない、ということが分かった気がします。
Split おせぇ。。

おまけ

自分で Split 作ったほうが早くね?

ということで作ってみた

    public static class StringExtensions
    {
        public static string[] FastSplit(this string s, char separator)
        {
            int sectionCount = 1;
            for(int i = 0; i < s.Length; i++)
            {
                if(s[i] == separator)
                {
                    sectionCount++;
                }
            }

            var sSpan = s.AsSpan();
            var result = new string[sectionCount];
            int index = 0;
            int offset = 0;
            int length = 0;
            for(int i = 0; i < s.Length; i++)
            {
                length++;
                if(s[i] == separator)
                {
                    result[index++] = new string(sSpan.Slice(offset, length - 1));
                    length = 0;
                    offset = i + 1;
                }
            }
            result[index] = new string(sSpan.Slice(offset, length));
            return result;
        }
    }
     Method |       Mean |    Error |   StdDev | Allocated |

--------------- |-----------:|---------:|---------:|----------:|
UsingSplit | 1,057.0 ns | 168.6 ns | 252.4 ns | 0 B |
UsingFastSplit | 872.9 ns | 151.5 ns | 226.8 ns | 0 B |

早くなりましたとさ。おわり。

Discussion