【问题标题】:Can I somehow hint the compiler to calculate constant expression for me in C#?我可以以某种方式提示编译器在 C# 中为我计算常量表达式吗?
【发布时间】:2021-04-30 20:11:51
【问题描述】:

我在这里有以下struct

struct Time 
{
    public uint SEC;
    public uint MIN;
    public uint HOUR;
    public uint DAY;
}

现在我要计算整个秒数:

static uint F(Time t) 
{
    uint r = 0;
    
    r += t.DAY  * 24 * 60 * 60;
    r += t.HOUR * 60 * 60;
    r += t.MIN  * 60;
    r += t.SEC;
    
    return r; 
}

这里的问题是C# 为我生成了以下ASM

Program.F(Time)
    L0000: mov eax, [rcx+0xc]
    L0003: lea eax, [rax+rax*2]
    L0006: shl eax, 3
    L0009: imul eax, 0x3c       ; it literally multiples one by one, 
    L000c: imul eax, 0x3c       ; although they are int literals. 
    L000f: imul edx, [rcx+8], 0x3c
    L0013: imul edx, 0x3c
    L0016: add eax, edx
    L0018: imul edx, [rcx+4], 0x3c
    L001c: add eax, edx
    L001e: add eax, [rcx]
    L0020: ret

现在当我像这样手动计算它们时:

static uint G(Time t) 
{
    uint r = 0;
    
    r += t.DAY  * 86400;
    r += t.HOUR * 3600;
    r += t.MIN  * 60;
    r += t.SEC;
    
    return r; 
}

然后它会生成我正在寻找的结果:

Program.G(Time)
    L0000: imul eax, [rcx+0xc], 0x15180
    L0007: imul edx, [rcx+8], 0xe10
    L000e: add eax, edx
    L0010: imul edx, [rcx+4], 0x3c
    L0014: add eax, edx
    L0016: add eax, [rcx]
    L0018: ret

如您所见,它删除了多个IMULs(显然)。这是我正在寻找的ASM 输出。

问题

  • 我能否以某种方式提示C# 编译器为我计算整数文字?我想像第一个例子一样保持它,因为它对我来说更干净。这些示例(函数FG)是否有所不同,或者为什么C# 编译器决定不计算它们?

注意事项

【问题讨论】:

  • 为什么不使用现有的 TimeSpan 结构?这更有可能包含编译器/抖动优化。

标签: c# assembly x86-64


【解决方案1】:

如果在常量周围加上括号,编译器会简化表达式。

static uint F(Time t) 
{
    uint r = 0;
    
    r += t.DAY  * (24 * 60 * 60);
    r += t.HOUR * (60 * 60);
    r += t.MIN  * 60;
    r += t.SEC;
    
    return r; 
}

【讨论】:

  • 酷,不知道。 C# 编译器有这样的行为是有原因的吗?
  • 可能是因为乘法是从左到右计算的。 r += t.DAY * 24 * 60 * 60;r += (((t.DAY * 24) * 60) * 60); 相同,这 3 个乘法运算中的每一个都可能产生溢出错误的副作用,这可能是编译器不简化表达式的原因。
  • @JoelCoehoorn 在没有括号的情况下,乘法(就像 C# 中的大多数运算符一样)是左结合的,因此它表示 r += (((24 * 60) * 60) * t.DAY);。在这种情况下,C# 编译器也可以安全地简化为 86400。你问有没有? The answer is yes.
  • 当溢出行为是环绕时,整数乘法是关联的。一个好的编译器不需要帮助来进行这种优化,因为原始源代码与此等价。事实上,GCC -O2 做到了。 godbolt.org/z/Y5eEna7dP(我使用unsigned 来确保溢出被明确定义为包装,而不是编译器可能会忽略,因为有符号溢出是C 中的UB)。 C# 的 asm 检查错误,因此在这种情况下显然是错过了优化,并且表达式会在某个时候溢出一个大常量或多个常量。 ://
  • 但是,编译器的某些部分可能决定不重新关联和折叠常量,因为优化器通道由于某种原因没有考虑 * 关联。当* 与其他操作(尤其是)结合使用时,事情会变得复杂。 /,所以 JIT 可能不会尝试? OTOH,我们从Out-of-order execution in C# 知道,C# JIT 会将(a + b) + (c + d) 悲观化为a + b + c + d(一个串行 dep 链),这会在源中不存在任何符号溢出的情况下创建可能的签名溢出。所以它愿意重新关联其他操作。
猜你喜欢
  • 2015-07-05
  • 1970-01-01
  • 2020-02-22
  • 1970-01-01
  • 2023-03-18
  • 2012-06-14
  • 2017-07-21
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多