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されない程度に複雑な処理であれば、この程度の差は実処理の処理時間のノイズに紛れて多分差異が見いだせないかと。
なので、今回の件は、逆アセンブリで推測されることが実際差異として出てくるの?って気になって試したら実際目に見える形で差異が出てきたよていどの話にしかならないんじゃないかなと思います。
-
見る人が見ればこれ見ただけで何やりたいのか、どー言う結論なのかすぐわかっちゃう程度のネタばらしではあるw ↩