assembly 如何摆脱边界检查

2o7dmzc5  于 2023-04-12  发布在  其他
关注(0)|答案(2)|浏览(146)

有没有办法删除C#中的数组边界检查?
以下是我想达到目标:

public static int F(int[] M, int i) 
{
    return M[i]; // I can guarantee that [i] will never be outside of [0, M.Length]
}

在这个函数调用之前,我有一个已经检查边界的逻辑(其中有一些额外的逻辑)。我想删除的是以下几行:

Program.F(Int32[], Int32)
    L0000: sub rsp, 0x28
    L0004: cmp edx, [rcx+8]           ; I don't need this line
    L0007: jae short L0015            ; I don't need this line
    L0009: movsxd rax, edx
    L000c: mov eax, [rcx+rax*4+0x10]
    L0010: add rsp, 0x28
    L0014: ret
    L0015: call 0x00007ffc8877bc70    ; I don't need this line
    L001a: int3                       ; I don't need this line

提问

有没有办法删除这些指令?

注意事项

  • 我试图放一个if检查,希望编译器会得到它,但它使情况变得更糟。
public static int G(int[] M, int i) 
{
    if (i >= 0 && i < M.Length)
        return M[i];

    return -1;
}

这产生:

Program.G(Int32[], Int32)
    L0000: sub rsp, 0x28
    L0004: test edx, edx
    L0006: jl short L001f
    L0008: mov eax, [rcx+8]
    L000b: cmp eax, edx
    L000d: jle short L001f
    L000f: cmp edx, eax
    L0011: jae short L0029
    L0013: movsxd rax, edx
    L0016: mov eax, [rcx+rax*4+0x10]
    L001a: add rsp, 0x28
    L001e: ret
    L001f: mov eax, 0xffffffff
    L0024: add rsp, 0x28
    L0028: ret
    L0029: call 0x00007ffc8877bc70
    L002e: int3

你也看到了没什么用

  • 我能做的是:使用unsafe
public static unsafe int H(int* M, int i) 
{
    return M[i];
}

这就是我一直在寻找的东西:

Program.H(Int32*, Int32)
    L0000: movsxd rax, edx
    L0003: mov eax, [rcx+rax*4]
    L0006: ret

但是很遗憾我不能为我的项目启用不安全。在“非不安全”的世界里有解决方案吗?

5hcedyr0

5hcedyr01#

实际上有一种方法。在csFastFloat存储库中偶然发现的。
这里的想法是使用MemoryMarshall.GetArrayDataReference来获取数组中第一个元素的引用,然后添加shift来获取实际值:

[MethodImpl(MethodImplOptions.AggressiveInlining)]
static T FastAccessValue<T>(T[] ar, int index)
{
       ref T tableRef = ref MemoryMarshal.GetArrayDataReference(ar);
       return Unsafe.Add(ref tableRef, (nint)index);
}
  • safe*(?)相当于unsafe版本
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static unsafe T FastAccessValueUnsafe<T>(T[] ar, int index) where T : unmanaged
{
     fixed(T* ptr = ar)
     {
         return ptr[index];
     }
}

而不限于仅unmanaged结构。
通过不安全访问,它甚至可以在大数据(超过百万项)上执行速度提高10%

public int SumUnsafe(int[] ints, int length)
{
    int sum = 0;
    for (int i = 0; i < length; i++)
    {
        sum += FastAccessValue(ints, i);
    }
    return sum;
}
public int SumDirect(int[] ints, int length)
{
    int sum = 0;
    for (int i = 0; i < ints.Length; i++)
    {
        sum += ints[i];
    }
    return sum;
}
方法整数长度平均值错误标准差代码大小
SumDirectInt32[100000]10000080.13微秒0.748 μs0.700 μs29 B
SumUnsafeInt32[100000]十万81.99 μs0.535 μs0.446 μsB
SumDirectInt32[1000000]1000000854.73 μs5.216 μs4.624 μs29 B
SumUnsafeInt32[1000000]100万795.10 μs2.680 μs2.238 μsB
SumDirectInt32[10000000]1000000010,104.72 μs27.199 μs22.712 μs29 B
SumUnsafeInt32[10000000]1000万9,126.06 μs30.329 μs26.886 μsB

基准位于此gist

k4emjkb1

k4emjkb12#

根据这个博客:
https://devblogs.microsoft.com/dotnet/performance_improvements_in_net_7/#bounds-check-elimination
net7中的PGO特性极大地优化了边界检查。但是我怀疑任何人都能从理论上计算出它在你的情况下是如何工作的,因为我认为它是不确定的。
我将设置以下参数,然后使用和不使用基准:

DOTNET_TieredPGO=1
DOTNET_ReadyToRun=0

相关问题