LoginSignup
9

More than 3 years have passed since last update.

C#の重箱の隅をつつく ~引数の位置で処理効率がビミョーに変わる~

Last updated at Posted at 2019-09-12

TL;DR

public static int Add(int cx, int dx, int r8, int r9, int s1, int s2, int s3, int s4) 1

のような割とアレなメソッドが有ったとき、cx~r9に対する処理とs1~s4に対する処理で、前者の方がわずかに速かった。

但し、今回の検証は、逆アセンブリ眺めていて、ホントに差が出たら楽しいな的な、完全に逆側からのアプローチしてみて、ホントに差が出たね~。うれし~な✨って意味しか無いので、その辺ご了承の程

試してみたこと

    public class ParameterPositionBench
    {
        private const int Iteration = 800_000_000;

        [MethodImpl(MethodImplOptions.NoInlining)]
        public static int AddReg(int cx, int dx, int r8, int r9, int s1, int s2, int s3, int s4)
        {
            return cx + dx + r8 + r9;
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public static int AddStack(int cx, int dx, int r8, int r9, int s1, int s2, int s3, int s4)
        {
            return s1 + s2 + s3 + s4;
        }

        [Benchmark]
        public int UseRegister()
        {
            var accum = 0;

            for (int i = 0; i < Iteration; i++)
            {
                accum += AddReg(++i, ++i, ++i, ++i, ++i, ++i, ++i, ++i);
            }

            return accum;
        }

        [Benchmark]
        public int UseStack()
        {
            var accum = 0;

            for (int i = 0; i < Iteration; i++)
            {
                accum += AddStack(++i, ++i, ++i, ++i, ++i, ++i, ++i, ++i);
            }

            return accum;
        }
    }

こいつを試してみて差が出るか?と言うことで試したら、以下の結果を得た


BenchmarkDotNet=v0.11.5, OS=Windows 10.0.18362
AMD Ryzen 7 1700X, 1 CPU, 16 logical and 8 physical cores
.NET Core SDK=2.2.402
  [Host]     : .NET Core 2.2.7 (CoreCLR 4.6.28008.02, CoreFX 4.6.28008.03), 64bit RyuJIT
  DefaultJob : .NET Core 2.2.7 (CoreCLR 4.6.28008.02, CoreFX 4.6.28008.03), 64bit RyuJIT


Method Mean Error StdDev
UseRegister 240.7 ms 0.4538 ms 0.4245 ms
UseStack 261.6 ms 0.3216 ms 0.2851 ms

えらく微妙だけど差が出てるは出てる。

なぜ差が出るのか

ぱっと見た感じ、AddRegの方は、前半分の4つの引数を加算してるし、AddStackの方は、後ろ半分の4つを加算してるので、本来差が出そうにはない。

これがなんで差が出てしまうかというと、x64 calling conventionに書いてある

The first four integer arguments are passed in registers. Integer values are passed in left-to-right order in RCX, RDX, R8, and R9, respectively. Arguments five and higher are passed on the stack.

の通り、引数が整数の場合、最初の4つはレジスタ渡し、それ以降はスタック渡しになるので。

で、スタック渡しよりレジスタ渡しの方が速くなるんじゃなかろうか?

と適当に考えて適当に試してみたら本当に速かった。

インライン処理されると無意味に ~まとめにかえて~

今回の検証というか重箱の隅つついてみた結果は重箱の隅つついたらやっぱり差が出た程度の意味しかやっぱり無い。

推測も含むけど、この程度の極めて単純な処理であれば、概ね全てかL1キャッシュに乗るのでメモリとのやりとりにはならないだろうし、実際ならないからこそこの程度の差しか出なかったと考えられる。

そして、[MethodImpl(MethodImplOptions.NoInlining)]こいつを付けないと、実は全く同じ処理になるw

これは、NoInlining処理しないと極めて単純すぎるためInliningされて未使用変数の除去が発生し、結果同じバイナリになってしまう。

逆に、属性無しでもInliningされない程度に複雑な処理であれば、この程度の差は実処理の処理時間のノイズに紛れて多分差異が見いだせないかと。

なので、今回の件は、逆アセンブリで推測されることが実際差異として出てくるの?って気になって試したら実際目に見える形で差異が出てきたよていどの話にしかならないんじゃないかなと思います。


  1. 見る人が見ればこれ見ただけで何やりたいのか、どー言う結論なのかすぐわかっちゃう程度のネタばらしではあるw 

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
9