首页 文章

为什么总结一个值类型数组然后总结一个引用类型数组?

提问于
浏览
19

我'm trying to understand better how memory works in .NET, so I'正在玩BenchmarkDotNet and 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
}

我有三个数组:

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 是一个基准参数 . 另外两个基准方法( ValueTypeSumExtendedValueTypeSum )是相同的,除了我在 _valueTypeData_extendedValueTypeData 上求和 . Full code for the benchmark .

基准测试结果:

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 |

核心:.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 |

我使用 BranchMispredictionsCacheMisses 硬件计数器运行基准测试,但没有缓存未命中,也没有分支错误预测 . 我还检查了发布的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;
}

Full code here.

基准测试结果:

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 |

4 回答

  • 3

    在Haswell中,英特尔为小环路引入了分支预测的附加策略(这就是为什么我们无法在IvyBridge上观察到这种情况) . 似乎特定的分支策略取决于许多因素,包括本机代码对齐 . LegacyJIT和RyuJIT之间的区别可以通过方法的不同对齐策略来解释 . 不幸的是,我无法提供这种性能现象的所有相关细节(英特尔保密实施细节;我的结论仅基于我自己的CPU逆向工程实验),但我可以告诉你如何使这个基准更好 .

    改善结果的主要技巧是手动循环展开,这对Haswell和RyuJIT的纳米标记至关重要 . 上述现象仅影响小循环,因此我们可以通过庞大的循环体来解决问题 . 事实上,当你有一个像基准测试

    [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 |
    
  • 4

    我认为结果如此接近的原因是使用一个如此小的大小而不是在堆中(在数组初始化循环内)将任何东西分配给碎片对象数组元素 .

    在你的基准代码中,只有对象数组元素从堆中分配,这样MemoryAllocator可以在堆中顺序分配每个元素(**) . (*)当代码开始执行时,内存将从ram读取到cpu缓存,因为你的对象数组元素以顺序(以连续块)的顺序写入ram,它们将被缓存,这就是为什么你没有得到任何缓存未命中 .

    为了更好地看到这一点,您可以使用另一个将在堆上分配的数组来对基准数组元素进行分段 . 这可能导致缓存未命中发生在当前设置之前 .

    此外,您的基准代码中没有任何分支,因此您无法获得brach miss预测 .

    (*)ValueType数组使用 new ValueType[Size] 初始化数组元素时,分配所需的所有空间;

    (**)objectArr [i]元素和objectArr [i 1](依此类推)将在堆中并排,当ram块缓存时,可能所有的对象数组元素都将被读取到cpu缓存,所以没有ram迭代数组时将需要访问权限 .

  • 7

    这确实是一种非常奇怪的行为 .

    为引用类型生成的核心循环代码如下:

    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
    

    所以区别归结为:

    对于 reference type

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

    对于 value type

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

    使用一条指令而没有间接内存访问,值类型应该更快......

    我试图通过Intel Architecture Code Analyzer运行它, reference type 的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个循环对比1.74个循环)

    我不是解密IACA输出的专家,但我的猜测是它与端口使用有关(在2-3之间更好地分配参考类型)

    "port"是CPU中的微执行单元 . 例如,对于Skylake,他们就像这样划分(来自Instruction tables from Agner optimize resources

    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;
    }
    

    循环将稍微优化一下,您还应该获得更一致的结果 .

  • 4

    我查看了.NET Core 2.1 x64上的反汇编 .

    ref类型代码看起来最适合我 . 机器代码正在加载每个对象引用,然后从每个实例加载字段 .

    值类型变体具有数组范围检查 . 循环克隆没有成功 . 此范围检查是因为循环上限是 Size . 它应该是 array.Length ,以便JIT可以识别此模式并且不生成范围检查 .


    这是ref版本 . 我已经标记了核心循环 . 找到核心循环的技巧是首先找到循环顶部的后跳 .

    这是 Value 变体:

    jae 是范围检查 .


    所以这是一个JIT限制 . 如果您关心这个问题,请在coreclr存储库上打开一个GitHub问题,并告诉他们循环克隆在这里失败了 .

    4.7.2上的非遗留JIT具有相同的范围检查行为 . 生成的代码对于ref版本看起来相同:

    我没有看过传统的JIT代码,但我认为它无法消除任何范围检查 . 我相信它不支持循环克隆 .

相关问题