【问题标题】:How to get rid of bounds check如何摆脱边界检查
【发布时间】:2021-04-04 07:51:01
【问题描述】:

有没有办法删除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

但遗憾的是,我无法为我的项目启用 unsafe。 “非不安全”的世界有解决方案吗?

【问题讨论】:

  • 您是否分析过代码并得出边界检查实际上会大大减慢速度的结论? stackoverflow.com/questions/16713076/…
  • @trenki 是的,unsafe 版本比普通版本更快。但正如我所说,在我的项目中启用 unsafe 对我来说很难。也很难包含基准,因为它有很多依赖项,并且清除它们并将它们包含在我的问题中会花费太多时间(+ 我不认为更改代码会给我们带来准确的结果)。跨度>
  • 这能回答你的问题吗? Array bounds check efficiency in .net 4 and above
  • 这样短的方法在编译过程中被内联并且没有开销SharpLab
  • @Hrant。你是对的。我不习惯阅读asm,所以没听懂。做了一些基准测试 1_000_000 个整数:直接求和 0 .. ar.Length 花了 803μs,fsum length - 1 .. 0 891μs 并且没有检查优化 0 .. length 我的 PC 上的 918μs

标签: c# arrays assembly x86-64


【解决方案1】:

其实是有办法的。在csFastFloat 存储库中偶然发现它。

这里的想法是使用MemoryMarshall.GetArrayDataReference 获取对数组中第一项的引用,然后添加移位以获得实际值:

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

这是安全(?)相当于不安全版本

[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;
}
Method ints length Mean Error StdDev Code Size
SumDirect Int32[100000] 100000 80.13 μs 0.748 μs 0.700 μs 29 B
SumUnsafe Int32[100000] 100000 81.99 μs 0.535 μs 0.446 μs 33 B
SumDirect Int32[1000000] 1000000 854.73 μs 5.216 μs 4.624 μs 29 B
SumUnsafe Int32[1000000] 1000000 795.10 μs 2.680 μs 2.238 μs 33 B
SumDirect Int32[10000000] 10000000 10,104.72 μs 27.199 μs 22.712 μs 29 B
SumUnsafe Int32[10000000] 10000000 9,126.06 μs 30.329 μs 26.886 μs 33 B

基准位于此gist

【讨论】:

  • 但在较小的数组大小和较大的代码大小上速度较慢?他们编译成什么汇编?手动执行寻址模式?
  • @PeterCordes 它增加了两个死胡同操作:cmpmov pastebin.com/dzRJdLR4
  • 我想你忘了在你的循环中使用你的第二个参数length。正如 cmets 中所讨论的那样,它会有所作为。
  • @Hrant 有意显示 ideal 用于 JIT 代码和不安全代码之间的区别
  • @PeterCordes 如果他会使用参数length (这是我最初的例子),那么我们应该在这里获得更有效的行为。如果我错了,请纠正我。
猜你喜欢
  • 2012-01-01
  • 2016-04-14
  • 1970-01-01
  • 2016-11-29
  • 1970-01-01
  • 2016-11-09
  • 1970-01-01
  • 1970-01-01
  • 2021-08-17
相关资源
最近更新 更多