web-dev-qa-db-ja.com

値型の配列の合計が参照型の配列の合計よりも遅いのはなぜですか?

.NETでメモリがどのように機能するかをよりよく理解しようとしているので、 BenchmarkDotNetおよびdiagnozers で遊んでいます。配列項目を合計して、classstructのパフォーマンスを比較するベンチマークを作成しました。値の型の合計が常に速くなることを期待していました。しかし、短い配列の場合はそうではありません。誰かがそれを説明できますか?

コード:

internal class ReferenceType
{
    public int Value;
}

internal struct ValueType
{
    public int Value;
}

internal struct ExtendedValueType
{
    public int Value;
    private double _otherData; // this field is here just to make the object bigger
}

3つの配列があります。

    private ReferenceType[] _referenceTypeData;
    private ValueType[] _valueTypeData;
    private ExtendedValueType[] _extendedValueTypeData;

同じランダム値のセットで初期化します。

次に、ベンチマークされたメソッド:

    [Benchmark]
    public int ReferenceTypeSum()
    {
        var sum = 0;

        for (var i = 0; i < Size; i++)
        {
            sum += _referenceTypeData[i].Value;
        }

        return sum;
    }

Sizeはベンチマークパラメータです。他の2つのベンチマークメソッド(ValueTypeSumExtendedValueTypeSum)は同じですが、_valueTypeDataまたは_extendedValueTypeDataベンチマークの完全なコード

ベンチマーク結果:

DefaultJob:.NET Framework 4.7.2(CLR 4.0.30319.42000)、64ビットRyuJIT-v4.7.3190.0

               Method | Size |      Mean |     Error |    StdDev | Ratio | RatioSD |
--------------------- |----- |----------:|----------:|----------:|------:|--------:|
     ReferenceTypeSum |  100 |  75.76 ns | 1.2682 ns | 1.1863 ns |  1.00 |    0.00 |
         ValueTypeSum |  100 |  79.83 ns | 0.3866 ns | 0.3616 ns |  1.05 |    0.02 |
 ExtendedValueTypeSum |  100 |  78.70 ns | 0.8791 ns | 0.8223 ns |  1.04 |    0.01 |
                      |      |           |           |           |       |         |
     ReferenceTypeSum |  500 | 354.78 ns | 3.9368 ns | 3.6825 ns |  1.00 |    0.00 |
         ValueTypeSum |  500 | 367.08 ns | 5.2446 ns | 4.9058 ns |  1.03 |    0.01 |
 ExtendedValueTypeSum |  500 | 346.18 ns | 2.1114 ns | 1.9750 ns |  0.98 |    0.01 |
                      |      |           |           |           |       |         |
     ReferenceTypeSum | 1000 | 697.81 ns | 6.8859 ns | 6.1042 ns |  1.00 |    0.00 |
         ValueTypeSum | 1000 | 720.64 ns | 5.5592 ns | 5.2001 ns |  1.03 |    0.01 |
 ExtendedValueTypeSum | 1000 | 699.12 ns | 9.6796 ns | 9.0543 ns |  1.00 |    0.02 |

Core:.NET Core 2.1.4(CoreCLR 4.6.26814.03、CoreFX 4.6.26814.02)、64ビットRyuJIT

               Method | Size |      Mean |     Error |    StdDev | Ratio | RatioSD |
--------------------- |----- |----------:|----------:|----------:|------:|--------:|
     ReferenceTypeSum |  100 |  76.22 ns | 0.5232 ns | 0.4894 ns |  1.00 |    0.00 |
         ValueTypeSum |  100 |  80.69 ns | 0.9277 ns | 0.8678 ns |  1.06 |    0.01 |
 ExtendedValueTypeSum |  100 |  78.88 ns | 1.5693 ns | 1.4679 ns |  1.03 |    0.02 |
                      |      |           |           |           |       |         |
     ReferenceTypeSum |  500 | 354.30 ns | 2.8682 ns | 2.5426 ns |  1.00 |    0.00 |
         ValueTypeSum |  500 | 372.72 ns | 4.2829 ns | 4.0063 ns |  1.05 |    0.01 |
 ExtendedValueTypeSum |  500 | 357.50 ns | 7.0070 ns | 6.5543 ns |  1.01 |    0.02 |
                      |      |           |           |           |       |         |
     ReferenceTypeSum | 1000 | 696.75 ns | 4.7454 ns | 4.4388 ns |  1.00 |    0.00 |
         ValueTypeSum | 1000 | 697.95 ns | 2.2462 ns | 2.1011 ns |  1.00 |    0.01 |
 ExtendedValueTypeSum | 1000 | 687.75 ns | 2.3861 ns | 1.9925 ns |  0.99 |    0.01 |

BranchMispredictionsおよびCacheMissesハードウェアカウンターを使用してベンチマークを実行しましたが、キャッシュミスやブランチの予測ミスはありません。私はリリースILコードもチェックしました。ベンチマークメソッドは、参照変数または値型変数をロードする命令によってのみ異なります。

配列のサイズが大きい場合、値の型の配列の合計は常に速くなります(たとえば、値の型が占めるメモリが少ないため)が、短い配列の場合はなぜ遅いのかわかりません。ここで何が恋しいですか?そして、structを大きくすると(ExtendedValueTypeを参照)、合計が少し速くなるのはなぜですか?

----更新----

@usrのコメントに触発されて、LegacyJitでベンチマークを再実行しました。 @Silver Shroudから着想を得たメモリ診断機能も追加しました(そうです、ヒープの割り当てはありません)。

Job = LegacyJitX64 Jit = LegacyJit Platform = X64 Runtime = Clr

               Method | Size |       Mean |      Error |     StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op |
--------------------- |----- |-----------:|-----------:|-----------:|------:|--------:|------------:|------------:|------------:|--------------------:|
     ReferenceTypeSum |  100 |   110.1 ns |  0.6836 ns |  0.6060 ns |  1.00 |    0.00 |           - |           - |           - |                   - |
         ValueTypeSum |  100 |   109.5 ns |  0.4320 ns |  0.4041 ns |  0.99 |    0.00 |           - |           - |           - |                   - |
 ExtendedValueTypeSum |  100 |   109.5 ns |  0.5438 ns |  0.4820 ns |  0.99 |    0.00 |           - |           - |           - |                   - |
                      |      |            |            |            |       |         |             |             |             |                     |
     ReferenceTypeSum |  500 |   517.8 ns | 10.1271 ns | 10.8359 ns |  1.00 |    0.00 |           - |           - |           - |                   - |
         ValueTypeSum |  500 |   511.9 ns |  7.8204 ns |  7.3152 ns |  0.99 |    0.03 |           - |           - |           - |                   - |
 ExtendedValueTypeSum |  500 |   534.7 ns |  3.0168 ns |  2.8219 ns |  1.03 |    0.02 |           - |           - |           - |                   - |
                      |      |            |            |            |       |         |             |             |             |                     |
     ReferenceTypeSum | 1000 | 1,058.3 ns |  8.8829 ns |  8.3091 ns |  1.00 |    0.00 |           - |           - |           - |                   - |
         ValueTypeSum | 1000 | 1,048.4 ns |  8.6803 ns |  8.1196 ns |  0.99 |    0.01 |           - |           - |           - |                   - |
 ExtendedValueTypeSum | 1000 | 1,057.5 ns |  5.9456 ns |  5.5615 ns |  1.00 |    0.01 |           - |           - |           - |                   - |

従来のJITの結果は予想どおりですが、以前の結果よりも遅くなります。これは、RyuJitが魔法のようなパフォーマンスの改善を行っていることを示唆しています。

----更新2 ----

すばらしい回答をありがとう!私はたくさん学んだ!

さらに別のベンチマークの結果の下。 @usrと@xoofxで提案されているように、最初にベンチマークされたメソッド、最適化されたメソッドを比較しています。

[Benchmark]
public int ReferenceTypeOptimizedSum()
{
    var sum = 0;
    var array = _referenceTypeData;

    for (var i = 0; i < array.Length; i++)
    {
        sum += array[i].Value;
    }

    return sum;
} 

上記の最適化が追加された、@ AndreyAkinshinによって提案された展開されたバージョン:

[Benchmark]
public int ReferenceTypeUnrolledSum()
{
    var sum = 0;
    var array = _referenceTypeData;

    for (var i = 0; i < array.Length; i += 16)
    {
        sum += array[i].Value;
        sum += array[i + 1].Value;
        sum += array[i + 2].Value;
        sum += array[i + 3].Value;
        sum += array[i + 4].Value;
        sum += array[i + 5].Value;
        sum += array[i + 6].Value;
        sum += array[i + 7].Value;
        sum += array[i + 8].Value;
        sum += array[i + 9].Value;
        sum += array[i + 10].Value;
        sum += array[i + 11].Value;
        sum += array[i + 12].Value;
        sum += array[i + 13].Value;
        sum += array[i + 14].Value;
        sum += array[i + 15].Value;
    }

    return sum;
}

完全なコードはこちら

ベンチマーク結果:

BenchmarkDotNet = v0.11.3、OS = Windows 10.0.17134.345(1803/April2018Update/Redstone4)Intel Core i5-6400 CPU 2.70GHz(Skylake)、1 CPU、4つの論理コアと4つの物理コア= 2648439 Hz、解像度= 377.5809 ns、タイマー= TSC

DefaultJob:.NET Framework 4.7.2(CLR 4.0.30319.42000)、64ビットRyuJIT-v4.7.3190.0

                        Method | Size |     Mean |     Error |    StdDev | Ratio | RatioSD |
------------------------------ |----- |---------:|----------:|----------:|------:|--------:|
              ReferenceTypeSum |  512 | 344.8 ns | 3.6473 ns | 3.4117 ns |  1.00 |    0.00 |
                  ValueTypeSum |  512 | 361.2 ns | 3.8004 ns | 3.3690 ns |  1.05 |    0.02 |
          ExtendedValueTypeSum |  512 | 347.2 ns | 5.9686 ns | 5.5831 ns |  1.01 |    0.02 |

     ReferenceTypeOptimizedSum |  512 | 254.5 ns | 2.4427 ns | 2.2849 ns |  0.74 |    0.01 |
         ValueTypeOptimizedSum |  512 | 353.0 ns | 1.9201 ns | 1.7960 ns |  1.02 |    0.01 |
 ExtendedValueTypeOptimizedSum |  512 | 280.3 ns | 1.2423 ns | 1.0374 ns |  0.81 |    0.01 |

      ReferenceTypeUnrolledSum |  512 | 213.2 ns | 1.2483 ns | 1.1676 ns |  0.62 |    0.01 |
          ValueTypeUnrolledSum |  512 | 201.3 ns | 0.6720 ns | 0.6286 ns |  0.58 |    0.01 |
  ExtendedValueTypeUnrolledSum |  512 | 223.6 ns | 1.0210 ns | 0.9550 ns |  0.65 |    0.01 |
25

Haswellで、Intelは小さなループの分岐予測のための追加の戦略を導入しました(そのため、IvyBridgeでこの状況を観察できません)。特定の分岐戦略は、ネイティブコードの配置を含む多くの要因に依存しているようです。 LegacyJITとRyuJITの違いは、メソッドのさまざまなアライメント戦略によって説明できます。残念ながら、私はこのパフォーマンス現象の関連する詳細をすべて提供することはできません(インテルは実装の詳細を秘密にしています。私の結論は自分のCPUリバースエンジニアリング実験にのみ基づいています)。しかし、このベンチマークを改善する方法を説明できます。

結果を改善する主なトリックは、RyuJITを使用したHaswell +でのナノベンチマークにとって重要な手動ループ展開です。上記の現象は小さなループにのみ影響するため、巨大なループ本体の問題を解決できます。実際、次のようなベンチマークがある場合

[Benchmark]
public void MyBenchmark()
{
    Foo();
}

BenchmarkDotNetは次のループを生成します。

for (int i = 0; i < N; i++)
{
    Foo(); Foo(); Foo(); Foo();
    Foo(); Foo(); Foo(); Foo();
    Foo(); Foo(); Foo(); Foo();
    Foo(); Foo(); Foo(); Foo();
}

このループの内部呼び出しの数は、UnrollFactorを使用して制御できます。ベンチマーク内に小さなループがある場合は、同じ方法で展開する必要があります。

[Benchmark(Baseline = true)]
public int ReferenceTypeSum()
{
    var sum = 0;

    for (var i = 0; i < Size; i += 16)
    {
        sum += _referenceTypeData[i].Value;
        sum += _referenceTypeData[i + 1].Value;
        sum += _referenceTypeData[i + 2].Value;
        sum += _referenceTypeData[i + 3].Value;
        sum += _referenceTypeData[i + 4].Value;
        sum += _referenceTypeData[i + 5].Value;
        sum += _referenceTypeData[i + 6].Value;
        sum += _referenceTypeData[i + 7].Value;
        sum += _referenceTypeData[i + 8].Value;
        sum += _referenceTypeData[i + 9].Value;
        sum += _referenceTypeData[i + 10].Value;
        sum += _referenceTypeData[i + 11].Value;
        sum += _referenceTypeData[i + 12].Value;
        sum += _referenceTypeData[i + 13].Value;
        sum += _referenceTypeData[i + 14].Value;
        sum += _referenceTypeData[i + 15].Value;
    }

    return sum;
}

別のトリックは、積極的なウォームアップ(たとえば、30回の反復)です。これが私のマシンでのウォームアップステージの様子です。

WorkloadWarmup   1: 4194304 op, 865744000.00 ns, 206.4095 ns/op
WorkloadWarmup   2: 4194304 op, 892164000.00 ns, 212.7085 ns/op
WorkloadWarmup   3: 4194304 op, 861913000.00 ns, 205.4961 ns/op
WorkloadWarmup   4: 4194304 op, 868044000.00 ns, 206.9578 ns/op
WorkloadWarmup   5: 4194304 op, 933894000.00 ns, 222.6577 ns/op
WorkloadWarmup   6: 4194304 op, 890567000.00 ns, 212.3277 ns/op
WorkloadWarmup   7: 4194304 op, 923509000.00 ns, 220.1817 ns/op
WorkloadWarmup   8: 4194304 op, 861953000.00 ns, 205.5056 ns/op
WorkloadWarmup   9: 4194304 op, 862454000.00 ns, 205.6251 ns/op
WorkloadWarmup  10: 4194304 op, 862565000.00 ns, 205.6515 ns/op
WorkloadWarmup  11: 4194304 op, 867301000.00 ns, 206.7807 ns/op
WorkloadWarmup  12: 4194304 op, 841892000.00 ns, 200.7227 ns/op
WorkloadWarmup  13: 4194304 op, 827717000.00 ns, 197.3431 ns/op
WorkloadWarmup  14: 4194304 op, 828257000.00 ns, 197.4719 ns/op
WorkloadWarmup  15: 4194304 op, 812275000.00 ns, 193.6615 ns/op
WorkloadWarmup  16: 4194304 op, 792011000.00 ns, 188.8301 ns/op
WorkloadWarmup  17: 4194304 op, 792607000.00 ns, 188.9722 ns/op
WorkloadWarmup  18: 4194304 op, 794428000.00 ns, 189.4064 ns/op
WorkloadWarmup  19: 4194304 op, 794879000.00 ns, 189.5139 ns/op
WorkloadWarmup  20: 4194304 op, 794914000.00 ns, 189.5223 ns/op
WorkloadWarmup  21: 4194304 op, 794061000.00 ns, 189.3189 ns/op
WorkloadWarmup  22: 4194304 op, 793385000.00 ns, 189.1577 ns/op
WorkloadWarmup  23: 4194304 op, 793851000.00 ns, 189.2688 ns/op
WorkloadWarmup  24: 4194304 op, 793456000.00 ns, 189.1747 ns/op
WorkloadWarmup  25: 4194304 op, 794194000.00 ns, 189.3506 ns/op
WorkloadWarmup  26: 4194304 op, 793980000.00 ns, 189.2996 ns/op
WorkloadWarmup  27: 4194304 op, 804402000.00 ns, 191.7844 ns/op
WorkloadWarmup  28: 4194304 op, 801002000.00 ns, 190.9738 ns/op
WorkloadWarmup  29: 4194304 op, 797860000.00 ns, 190.2246 ns/op
WorkloadWarmup  30: 4194304 op, 802668000.00 ns, 191.3710 ns/op

デフォルトでは、BenchmarkDotNetはそのような状況を検出し、ウォームアップの反復回数を増やします。残念ながら、それが常に可能であるとは限りません(「単純な」ケースで「高速な」ウォームアップステージを持ちたいと仮定します)。

そして、これが私の結果です(更新されたベンチマークの完全なリストはここにあります: https://Gist.github.com/AndreyAkinshin/4c9e0193912c99c0b314359d5c5d0a4e ):

BenchmarkDotNet=v0.11.3, OS=macOS Mojave 10.14.1 (18B75) [Darwin 18.2.0]
Intel Core i7-4870HQ CPU 2.50GHz (Haswell), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.0.100-preview-009812
  [Host]     : .NET Core 2.0.5 (CoreCLR 4.6.0.0, CoreFX 4.6.26018.01), 64bit RyuJIT
  Job-IHBGGW : .NET Core 2.0.5 (CoreCLR 4.6.0.0, CoreFX 4.6.26018.01), 64bit RyuJIT

IterationCount=30  WarmupCount=30  

               Method | Size |     Mean |     Error |    StdDev |   Median | Ratio | RatioSD |
--------------------- |----- |---------:|----------:|----------:|---------:|------:|--------:|
     ReferenceTypeSum |  256 | 180.7 ns | 0.4514 ns | 0.6474 ns | 180.8 ns |  1.00 |    0.00 |
         ValueTypeSum |  256 | 154.4 ns | 1.8844 ns | 2.8205 ns | 153.3 ns |  0.86 |    0.02 |
 ExtendedValueTypeSum |  256 | 183.1 ns | 2.2283 ns | 3.3352 ns | 181.1 ns |  1.01 |    0.02 |
10
AndreyAkinshin

これは確かに非常に奇妙な動作です。

参照型のコアループの生成コードは次のとおりです:

M00_L00:
mov     r9,rcx
cmp     edx,[r9+8]
jae     ArrayOutOfBound
movsxd  r10,edx
mov     r9,[r9+r10*8+10h]
add     eax,[r9+8]
inc     edx
cmp     edx,r8d
jl      M00_L00

一方、値型ループの場合:

M00_L00:
mov     r9,rcx
cmp     edx,[r9+8]
jae     ArrayOutOfBound
movsxd  r10,edx
add     eax,[r9+r10*4+10h]
inc     edx
cmp     edx,r8d
jl      M00_L00

つまり、違いは次のようになります。

参照タイプの場合:

mov     r9,[r9+r10*8+10h]
add     eax,[r9+8]

value typeの場合:

add     eax,[r9+r10*4+10h]

1つの命令と間接的なメモリアクセスがない場合、値のタイプはより高速になるはずです...

これを Intel Architecture Code Analyzer で実行しようとしましたが、参照タイプのIACA出力は次のとおりです。

Throughput Analysis Report
--------------------------
Block Throughput: 1.72 Cycles       Throughput Bottleneck: Dependency chains
Loop Count:  35
Port Binding In Cycles Per Iteration:
--------------------------------------------------------------------------------------------------
|  Port  |   0   -  DV   |   1   |   2   -  D    |   3   -  D    |   4   |   5   |   6   |   7   |
--------------------------------------------------------------------------------------------------
| Cycles |  1.0     0.0  |  1.0  |  1.5     1.5  |  1.5     1.5  |  0.0  |  1.0  |  1.0  |  0.0  |
--------------------------------------------------------------------------------------------------

DV - Divider pipe (on port 0)
D - Data fetch pipe (on ports 2 and 3)
F - Macro Fusion with the previous instruction occurred
* - instruction micro-ops not bound to a port
^ - Micro Fusion occurred
# - ESP Tracking sync uop was issued
@ - SSE instruction followed an AVX256/AVX512 instruction, dozens of cycles penalty is expected
X - instruction not supported, was not accounted in Analysis

| Num Of   |                    Ports pressure in cycles                         |      |
|  Uops    |  0  - DV    |  1   |  2  -  D    |  3  -  D    |  4   |  5   |  6   |  7   |
-----------------------------------------------------------------------------------------
|   1*     |             |      |             |             |      |      |      |      | mov r9, rcx
|   2^     |             |      | 0.5     0.5 | 0.5     0.5 |      | 1.0  |      |      | cmp edx, dword ptr [r9+0x8]
|   0*F    |             |      |             |             |      |      |      |      | jnb 0x22
|   1      |             |      |             |             |      |      | 1.0  |      | movsxd r10, edx
|   1      |             |      | 0.5     0.5 | 0.5     0.5 |      |      |      |      | mov r9, qword ptr [r9+r10*8+0x10]
|   2^     | 1.0         |      | 0.5     0.5 | 0.5     0.5 |      |      |      |      | add eax, dword ptr [r9+0x8]
|   1      |             | 1.0  |             |             |      |      |      |      | inc edx
|   1*     |             |      |             |             |      |      |      |      | cmp edx, r8d
|   0*F    |             |      |             |             |      |      |      |      | jl 0xffffffffffffffe6
Total Num Of Uops: 9

value typeの場合:

Throughput Analysis Report
--------------------------
Block Throughput: 1.74 Cycles       Throughput Bottleneck: Dependency chains
Loop Count:  26
Port Binding In Cycles Per Iteration:
--------------------------------------------------------------------------------------------------
|  Port  |   0   -  DV   |   1   |   2   -  D    |   3   -  D    |   4   |   5   |   6   |   7   |
--------------------------------------------------------------------------------------------------
| Cycles |  1.0     0.0  |  1.0  |  1.0     1.0  |  1.0     1.0  |  0.0  |  1.0  |  1.0  |  0.0  |
--------------------------------------------------------------------------------------------------

DV - Divider pipe (on port 0)
D - Data fetch pipe (on ports 2 and 3)
F - Macro Fusion with the previous instruction occurred
* - instruction micro-ops not bound to a port
^ - Micro Fusion occurred
# - ESP Tracking sync uop was issued
@ - SSE instruction followed an AVX256/AVX512 instruction, dozens of cycles penalty is expected
X - instruction not supported, was not accounted in Analysis

| Num Of   |                    Ports pressure in cycles                         |      |
|  Uops    |  0  - DV    |  1   |  2  -  D    |  3  -  D    |  4   |  5   |  6   |  7   |
-----------------------------------------------------------------------------------------
|   1*     |             |      |             |             |      |      |      |      | mov r9, rcx
|   2^     |             |      | 1.0     1.0 |             |      | 1.0  |      |      | cmp edx, dword ptr [r9+0x8]
|   0*F    |             |      |             |             |      |      |      |      | jnb 0x1e
|   1      |             |      |             |             |      |      | 1.0  |      | movsxd r10, edx
|   2      | 1.0         |      |             | 1.0     1.0 |      |      |      |      | add eax, dword ptr [r9+r10*4+0x10]
|   1      |             | 1.0  |             |             |      |      |      |      | inc edx
|   1*     |             |      |             |             |      |      |      |      | cmp edx, r8d
|   0*F    |             |      |             |             |      |      |      |      | jl 0xffffffffffffffea
Total Num Of Uops: 8

したがって、参照タイプにはわずかな利点があります(ループあたり1.72サイクルvs 1.74サイクル)

私はIACA出力の解読の専門家ではありませんが、それはポートの使用状況に関連していると思います(参照タイプの場合は2〜3のほうが適切に分散されます)。

「ポート」は、CPU内のマイクロ実行ユニットです。たとえばSkylakeの場合、これらは次のように分割されます(から Agnerの最適化リソースからの命令テーブル

Port 0: Integer, f.p. and vector ALU, mul, div, branch
Port 1: Integer, f.p. and vector ALU
Port 2: Load
Port 3: Load
Port 4: Store
Port 5: Integer and vector ALU
Port 6: Integer ALU, branch
Port 7: Store address

非常に微妙なマイクロ命令(uop)最適化のように見えますが、その理由を説明することはできません。

次のようにループのcodegenを改善できることに注意してください。

[Benchmark]
public int ValueTypeSum()
{
    var sum = 0;

    // NOTE: Caching the array to a local variable (that will avoid the reload of the Length inside the loop)
    var arr = _valueTypeData;
    // NOTE: checking against `array.Length` instead of `Size`, to completely remove the ArrayOutOfBound checks
    for (var i = 0; i < arr.Length; i++)
    {
        sum += arr[i].Value;
    }

    return sum;
}

ループは少しだけ最適化され、より一貫した結果が得られるはずです。

7
xoofx

結果がこのように近い理由は、非常に小さいサイズを使用し、オブジェクト配列要素をフラグメント化するために(配列初期化ループ内の)ヒープに何も割り当てていないためです。

ベンチマークコードでは、オブジェクト配列要素のみがヒープから割り当てられます(*)。このようにして、MemoryAllocatorは各要素をヒープ内で順次(**)割り当てることができます。ベンチマークコードの実行が開始されると、データがRAMからCPUキャッシュに読み込まれ、オブジェクト配列要素がRAMにシーケンシャル(連続したブロック内)の順序で書き込まれるため、キャッシュされ、キャッシュミスが発生しません。

これをよりよく理解するために、ベンチマークされたオブジェクト配列要素をフラグメント化するためにヒープに割り当てる別のオブジェクト配列(できれば、より大きなオブジェクト)を使用できます。これにより、現在の設定よりも早くキャッ​​シュミスが発生する可能性があります。実際のシナリオでは、同じヒープに割り当てられ、配列の実際のオブジェクトをさらに断片化する他のスレッドがあります。また、RAMへのアクセスには、CPUキャッシュ(またはCPUサイクル)へのアクセスよりも多くの時間がかかります。 (このトピックに関しては post を確認してください)。

(*)ValueType配列は、new ValueType[Size]で初期化すると、配列要素に必要なすべてのスペースを割り当てます。 ValueType配列要素はRAM内で隣接します。

(**)objectArr [i]オブジェクト要素とobjectArr [i + 1](以下同様)はヒープ内で並んでいます。RAMブロックがキャッシュされると、おそらくすべてのオブジェクト配列要素がCPUキャッシュに読み込まれるため、配列を反復処理する場合、RAMアクセスは必要ありません。

5
miskender

.NET Core 2.1 x64で逆アセンブリを確認しました。

Refタイプのコードは私には最適に見えます。マシンコードは、各オブジェクト参照をロードしてから、各インスタンスからフィールドをロードしています。

値タイプのバリアントには、配列範囲チェックがあります。ループの複製は成功しませんでした。ループの上限がSizeであるため、この範囲チェックが行われます。そのはず array.Length JITがこのパターンを認識し、範囲チェックを生成しないようにするため。


これは参照バージョンです。コアループにマークを付けました。コアループを見つけるコツは、最初にループの先頭へのバックジャンプを見つけることです。

enter image description here

これは値のバリアントです:

enter image description here

jaeは範囲チェックです。


したがって、これはJITの制限です。これに関心がある場合は、coreclrリポジトリでGitHubの問題を開き、ループのクローン作成がここで失敗したことを伝えます。

4.7.2の非レガシーJITの範囲チェック動作は同じです。生成されたコードは、refバージョンでも同じように見えます。

enter image description here

私はレガシーJITコードを見たことはありませんが、範囲チェックを排除できなかったと思います。ループクローンはサポートされていません。

4
usr