C# メソッドの戻り値にする変数は最初に宣言する
はじめに
C# で戻り値にする変数を最初に宣言することで、メソッドのパフォーマンスを改善することができます。
しかし、この差はごくわずかであり、通常は無視しても問題ありません。
例
最初の変数の宣言を入れ替えた 2 つのメソッドを用意します。
public static int SumA(int[] array)
{
int i = 0;
int sum = 0;
for (; i < array.Length; i++)
{
sum += array[i];
}
return sum;
}
public static int SumB(int[] array)
{
int sum = 0;
int i = 0;
for (; i < array.Length; i++)
{
sum += array[i];
}
return sum;
}
この例だとループ変数 i
を for
ステートメントの外側にすることはないと思いますが、他にいい例が思いつかなかったのでご容赦ください。
SumA
が戻り値を最初に宣言しないパターン、 SumB
が戻り値を最初に宣言するパターンです。
これらを SharpLab を使ってアセンブリーコードを出力します。
SumA(Int32[])
; int i;
L0000: xor eax, eax
; int sum;
L0002: xor edx, edx
L0004: mov r8d, [rcx+8]
L0008: test r8d, r8d
L000b: jle short L001c
L000d: mov r9d, eax
L0010: add edx, [rcx+r9*4+0x10]
L0015: inc eax
L0017: cmp r8d, eax
L001a: jg short L000d
; ここがよくない。
L001c: mov eax, edx
L001e: ret
SumB(Int32[])
; int sum;
L0000: xor eax, eax
; int i;
L0002: xor edx, edx
L0004: mov r8d, [rcx+8]
L0008: test r8d, r8d
L000b: jle short L001c
L000d: mov r9d, edx
L0010: add eax, [rcx+r9*4+0x10]
L0015: inc edx
L0017: cmp r8d, edx
L001a: jg short L000d
; mov 命令がない。
L001c: ret
SumA
のループ変数 i
が eax レジスター
に割り当てられてられているので、リターンの直前(L001c
のところ)で sum (edx レジスター)
を eax レジスター
に移動する必要があります。
対して SumB
は sum
が eax
に割り当てられるため、リターンの直前で eax
を移動する必要がありません。
つまり、戻り値にする変数が最初に宣言されていると、mov
命令が 1 つ削減されることがわかります。
ただし、mov
命令は非常に高速(レイテンシー 0.5)で、命令が存在はするので差がまったくないとは言えませんが、体感できるようなことはないと考えられます。(ピコ秒オーダーの世界)
コンパイラーがなぜ戻り値を rax レジスター
に割り当てる最適化ができないのかご存じの方はコメントください。
ベンチマーク
BenchmarkDotNet を使用してベンチマークを行いました。
Method | Length | Mean | Error | StdDev | Ratio | RatioSD | Code Size |
---|---|---|---|---|---|---|---|
SumA | 0 | 0.4977 ns | 0.0025 ns | 0.0022 ns | 1.00 | 0.00 | 41 B |
SumB | 0 | 0.5031 ns | 0.0027 ns | 0.0025 ns | 1.01 | 0.01 | 39 B |
SumA | 1 | 0.4032 ns | 0.0008 ns | 0.0007 ns | 1.00 | 0.00 | 41 B |
SumB | 1 | 0.2480 ns | 0.0004 ns | 0.0004 ns | 0.62 | 0.00 | 39 B |
SumA | 2 | 0.4989 ns | 0.0051 ns | 0.0045 ns | 1.00 | 0.00 | 41 B |
SumB | 2 | 0.4990 ns | 0.0029 ns | 0.0026 ns | 1.00 | 0.01 | 39 B |
SumA | 3 | 0.7568 ns | 0.0048 ns | 0.0043 ns | 1.00 | 0.00 | 41 B |
SumB | 3 | 0.7549 ns | 0.0099 ns | 0.0088 ns | 1.00 | 0.01 | 39 B |
SumA | 4 | 1.0647 ns | 0.0426 ns | 0.0399 ns | 1.00 | 0.00 | 41 B |
SumB | 4 | 1.0280 ns | 0.0139 ns | 0.0130 ns | 0.97 | 0.04 | 39 B |
SumA | 5 | 1.3225 ns | 0.0160 ns | 0.0149 ns | 1.00 | 0.00 | 41 B |
SumB | 5 | 1.3357 ns | 0.0119 ns | 0.0111 ns | 1.01 | 0.02 | 39 B |
SumA | 6 | 1.8165 ns | 0.0205 ns | 0.0171 ns | 1.00 | 0.00 | 41 B |
SumB | 6 | 1.8034 ns | 0.0056 ns | 0.0053 ns | 0.99 | 0.01 | 39 B |
SumA | 7 | 2.1148 ns | 0.0055 ns | 0.0049 ns | 1.00 | 0.00 | 41 B |
SumB | 7 | 2.2605 ns | 0.0274 ns | 0.0256 ns | 1.07 | 0.01 | 39 B |
SumA | 8 | 2.4482 ns | 0.0439 ns | 0.0411 ns | 1.00 | 0.00 | 41 B |
SumB | 8 | 2.3292 ns | 0.0207 ns | 0.0193 ns | 0.95 | 0.02 | 39 B |
SumA | 9 | 3.0970 ns | 0.0040 ns | 0.0038 ns | 1.00 | 0.00 | 41 B |
SumB | 9 | 2.9088 ns | 0.0249 ns | 0.0233 ns | 0.94 | 0.01 | 39 B |
SumA | 10 | 2.8354 ns | 0.0204 ns | 0.0191 ns | 1.00 | 0.00 | 41 B |
SumB | 10 | 2.9069 ns | 0.0486 ns | 0.0431 ns | 1.03 | 0.02 | 39 B |
SumA | 20 | 5.9170 ns | 0.1484 ns | 0.1876 ns | 1.00 | 0.00 | 41 B |
SumB | 20 | 5.6792 ns | 0.0694 ns | 0.0649 ns | 0.97 | 0.03 | 39 B |
なぜか配列の要素数が 1 のときだけ SumB
の方がだいぶ速くなっています。複数回実行しても比率はだいたい同じです。
BenchmarkDotNet が出力するアセンブリーコードも上記のものと同様なので、なぜこれほどの時間差があるのかさっぱりわかりません。
それ以外の要素数については、要素数によって SumA
が速かったり SumB
が速かったりと、安定していません。
またベンチマークし直すと結果が入れ替わったりします。
mov
命令の有無以外の別の要因が影響しているのかなと思っています。
そのためこの結果からだけだと mov
命令の有無がパフォーマンスにどう影響しているか結論付けられないと思います。
条件分岐が入っているので CPU の分岐予測が常に同じ結果になっていないのではと予想しています。
ソースコード
Discussion