首页 > 解决方案 > 为什么循环 Array.AsSpan() 更快?

问题描述

|         Method |     Mean |    Error |   StdDev |
|--------------- |---------:|---------:|---------:|
|  ArrayRefIndex | 661.9 us | 12.95 us | 15.42 us |
| ArraySpanIndex | 640.4 us |  4.08 us |  3.82 us |

为什么循环array.AsSpan()比直接循环源数组更快?

public struct Struct16
{
    public int A;
    public int B;
    public int C;
    public int D;
}

public class Program
{
    public const int COUNT = 100000;
    
    static void Main(string[] args)
    {
        var summary = BenchmarkRunner.Run<Program>();
    }

    [Benchmark]
    public int ArrayRefIndex()
    {
        Struct16[] myArray = new Struct16[COUNT];
        int sum = 0;
        for (int i = 0; i < myArray.Length; i++)
        {
            ref Struct16 value = ref myArray[i];
            sum += value.A = value.A + value.B + value.C + value.D;
        }
        return sum;
    }

    [Benchmark]
    public int ArraySpanIndex()
    {
        Struct16[] myArray = new Struct16[COUNT];
        int sum = 0;
        Span<Struct16> span = myArray.AsSpan();
        for (int i = 0; i < span.Length; i++)
        {
            ref Struct16 value = ref span[i];
            sum += value.A = value.A + value.B + value.C + value.D;
        }
        return sum;
    }
}

标签: c#arraysperformancebenchmarking

解决方案


简答

Span 保证了“任意内存的连续区域”,它允许编译器对 CLI 指令进行优化。

长答案

如果您在反汇编中打开您提供的代码(调试 -> Windows -> 反汇编),您将在 ArrayRefIndex() 中找到以下内容

ref Struct16 value = ref myArray[i];
00007FFC3E860DCC  movsxd      r8,ecx  
00007FFC3E860DCF  shl         r8,4  
00007FFC3E860DD3  lea         r8,[rax+r8+10h] // <----

“lea”代表加载有效地址。这意味着, ArrayRefIndex 函数较慢,因为它将结构数组视为无序内存

当我们查看 ArraySpanIndex 时,我们可以看到它没有“lea”指令,而是仅用“add”替换。我没有确认,但这很可能只是为下一个内存位置添加结构长度。无论哪种方式,“lea”指令是两个函数之间的唯一增量,将罪魁祸首缩小到时间差。

ref Struct16 value = ref span[i];
00007FFC3E8613FA  movsxd      r8,ecx  
00007FFC3E8613FD  shl         r8,4  
00007FFC3E861401  add         r8,rax  // <----

推荐阅读