【问题标题】:Is there restrict equivalent in C#C# 中是否有等价的限制
【发布时间】:2021-04-24 09:45:01
【问题描述】:

我有以下函数(我对其进行了一些清理以使其更易于理解),它将目标数组获取索引n 处的元素添加到它src1[i],然后将它与src2[i] 相乘(没什么花哨的):

static void F(long[] dst, long[] src1, long[] src2, ulong n) 
{
    dst[n] += src1[n];
    dst[n] *= src2[n];
}

不,这会生成以下ASM

<Program>$.<<Main>$>g__F|0_0(Int64[], Int64[], Int64[], UInt64)
    L0000: sub rsp, 0x28
    L0004: test r9, r9
    L0007: jl short L0051
    L0009: mov rax, r9
    L000c: mov r9d, [rcx+8]
    L0010: movsxd r9, r9d
    L0013: cmp rax, r9
    L0016: jae short L0057
    L0018: lea rcx, [rcx+rax*8+0x10]
    L001d: mov r9, rcx
    L0020: mov r10, [r9]
    L0023: mov r11d, [rdx+8]
    L0027: movsxd r11, r11d
    L002a: cmp rax, r11
    L002d: jae short L0057
    L002f: add r10, [rdx+rax*8+0x10]
    L0034: mov [r9], r10
    L0037: mov edx, [r8+8]
    L003b: movsxd rdx, edx
    L003e: cmp rax, rdx
    L0041: jae short L0057
    L0043: imul r10, [r8+rax*8+0x10]
    L0049: mov [rcx], r10
    L004c: add rsp, 0x28
    L0050: ret
    L0051: call 0x00007ffc9dadb710
    L0056: int3
    L0057: call 0x00007ffc9dadbc70
    L005c: int3

你可以添加一堆东西,因为我可以保证n 将在合法范围之间:我可以使用指针。

static unsafe void G(long* dst, long* src1, long* src2, ulong n) 
{
    dst[n] += src1[n];
    dst[n] *= src2[n];
}

现在这会生成更简单的ASM

<Program>$.<<Main>$>g__G|0_1(Int64*, Int64*, Int64*, UInt64)
    L0000: lea rax, [rcx+r9*8]
    L0004: mov rcx, rax
    L0007: mov rdx, [rdx+r9*8]
    L000b: add [rcx], rdx
    L000e: mov rdx, [rax]               ; loads the value again?
    L0011: imul rdx, [r8+r9*8]
    L0016: mov [rax], rdx
    L0019: ret

您可能已经注意到,那里有一个额外的MOV(我想,至少我无法解释为什么会在那里)。

问题

  • 如何删除该行?在C 中,如果我没记错的话,我可以使用关键字restrict。 C#中有这样的关键字吗?很遗憾,我在互联网上找不到任何东西。

注意

  • 这里是SharpLab 链接。
  • 这是C 示例:
void
f(int64_t  *dst, 
  int64_t  *src1, 
  int64_t  *src2, 
  uint64_t  n) {
        dst[n] += src1[n];
        dst[n] *= src2[n];
}

void
g(int64_t *restrict dst, 
  int64_t *restrict src1,  
  int64_t *restrict src2, 
  uint64_t          n) {
        dst[n] += src1[n];
        dst[n] *= src2[n];
}

这会生成:

f:
        mov     r10, rdx
        lea     rdx, [rcx+r9*8]
        mov     rax, QWORD PTR [rdx]
        add     rax, QWORD PTR [r10+r9*8]
        mov     QWORD PTR [rdx], rax       ; this is strange. It loads the value back to [RDX]?
                                           ; shouldn't that be other way around? I don't know.
        imul    rax, QWORD PTR [r8+r9*8]
        mov     QWORD PTR [rdx], rax
        ret

g:
        mov     r10, rdx
        lea     rdx, [rcx+r9*8]
        mov     rax, QWORD PTR [rdx]
        add     rax, QWORD PTR [r10+r9*8]
        imul    rax, QWORD PTR [r8+r9*8]
        mov     QWORD PTR [rdx], rax
        ret

这里是Godbolt 链接。

【问题讨论】:

  • mov [rdx], rax 存储(不加载)值回到dst[n]以防src2[n]相同的内存位置( imul rax, [r8+r9*8] 读取)。当我们知道它不可能是同一个位置时(因为消息来源承诺使用restrict),可以省略 store。不知道“其他方式”是什么意思,但除了比较地址和分支之外,我认为没有其他合理的方法可以解决可能的别名。
  • @PeterCordes 我的意思是:MOV RAX, [RDX]。因为它正在使用RAX,所以我认为它应该再次加载RAX 中的值。可能我在这里错了。
  • 请注意 C# 和 C 编译器输出之间的区别。您的 C# 输出正在使用内存 destination 添加,因此它愚蠢地必须从它知道它刚刚写入 (dst[n]) 以及 src2[n] 的位置重新加载.这很愚蠢,它知道那里有什么价值(因为我认为 C# 线程模型允许它假设没有其他线程写入该位置)。 (请注意,C# asm 无缘无故地将 LEA 结果从 RAX 复制到 RCX,因此 [rax][rcx] 实际上只是被取消引用两次的同一个指针。在 C 中没有限制,您只会得到一个额外的存储和相同的加载。

标签: c# assembly x86-64 restrict-qualifier


【解决方案1】:

这个:

dst[n] = (dst[n] + src1[n]) * src2[n];

删除多余的mov

C# 中,没有与C 语言中的restrict 等价的限定符。

C# ECMA-334:2017 语言规范中,在章节23. Unsafe Code 中,没有语法指定必须只能通过特定指针访问内存的一部分。并且没有语法来指定指针指向的内存区域不重叠。因此没有这样的等价物。这可能是因为 C# 是一种托管语言,unsafe 语法允许使用指针/非托管内存是 C# 中的一个边缘情况。而指针上的restrict 将是边缘情况的边缘情况。

【讨论】:

  • 确实如此。但为什么?在我的 C 示例中也是如此。您提供的组合版本有何不同?
  • @Hrant:因为它在写入任何地方之前读取了两个/所有源,所以别名不再意味着src2[n] 可能是dst[n] 的重新加载。 (如果它解释了这将是一个更好的答案。OTOH,解释可能的别名对编译器优化的实际影响是一个很大的弯路,而不是询问 C# 是否支持 restrict 等效项,而不是通常将内容读入本地变量或临时变量在其他商店之前。也许找到一个 C 或 C++ 问答,用示例解释它,这样你就可以从这个问题中删除那个附带问题)
  • @Renat:不可能有两个 不同的 对象重叠,但在 OP 的示例中,访问位于同一索引处,dst[n]src2[n] 如果被称为 F(a, b, a, 123)。 (即 两次传递 same 数组对象,因为两个不同的 args 会在没有 unsafe 的情况下创建别名。)除非函数可以内联到足以查看它们的数组来自哪里(例如单独分配),他们需要假设别名。
  • 我猜restrict 不值得,因为当前的 C# JIT 实现不会利用它。在 OP 的示例中,它甚至会无缘无故地重新加载 dst[n]。 (C 编译器在可能有别名时在稍后加载之前编写它,但重用来自寄存器的已知值。它不是 volatileatomic 所以他们可以假设没有其他线程更改了该值,就像 C# 编译器可以如果它足够聪明。)
  • @PeterCordes,感谢您提到两次传递相同指针的情况,我完全错过了。删除了关于不同对象内存的声明,因为它无关紧要
猜你喜欢
  • 1970-01-01
  • 2013-06-21
  • 1970-01-01
  • 2021-05-14
  • 1970-01-01
  • 1970-01-01
  • 2021-08-04
  • 2012-12-17
  • 1970-01-01
相关资源
最近更新 更多