首页 文章

为什么编译器优化ldc.i8而不是ldc.r8?

提问于
浏览
9

我想知道为什么这个C#代码

long b = 20;

被编译为

ldc.i4.s 0x14
conv.i8

(因为它需要3个字节而不是 ldc.i8 20 所需的9个字节 . 有关详细信息,请参阅this . )

而这段代码

double a = 20;

被编译为9字节指令

ldc.r8 20

而不是这个3字节的序列

ldc.i4.s 0x14
conv.r8

(使用mono 4.8 . )

这是错失的机会还是 conv.i8 的成本与代码大小的增加失衡?

3 回答

  • 3

    我怀疑你会得到比“没有人认为有必要实施它”更令人满意的答案 .

    事实是,他们可以获得的收益不会超过成本,例如:额外测试, intfloat 之间的非平凡转换,而在 ldc.i4.s 的情况下,'s not that much of a trouble. Also it'最好不要使用更多优化规则来破坏抖动 .

    Roslyn source code所示,转换仅针对 long 进行 . 总而言之,完全可以为 floatdouble 添加此功能,但除了生成更短的CIL代码(在需要内联时很有用),并且当您想要使用浮点常量时,它将没有多大用处,通常实际使用浮点数(即不是整数) .

  • 7

    首先,让我们考虑正确性 . ldc.i4.s 可以处理-128到127之间的整数,所有这些都可以在 float32 中精确表示 . 但是,CIL对某些存储位置使用名为 F 的内部浮点类型 . ECMA-335标准在III.1.1.1中说:

    ...变量或表达式的名义类型是float32或float64 ......内部表示应具有以下特征:内部表示应具有精度和范围大于或等于标称类型 . 来往内部代表的转换应保持 Value .

    这意味着无论 F 是什么,都保证在 F 中安全地表示任何 float32 值 .

    我们得出结论,您提出的替代指令序列是正确的 . 现在的问题是:在性能方面是否更好?

    要回答这个问题,让我们看看JIT编译器在看到两个代码序列时的作用 . 使用 ldc.r8 20 时,您引用的链接中给出的答案很好地解释了使用长指令的后果 .

    让我们考虑3字节序列:

    ldc.i4.s 0x14
    conv.r8
    

    我们可以在这里做出一个对任何优化JIT编译器都合理的假设 . 我们是补充格式,必须将其转换为 float32 格式(如上所述,它始终是安全的) . 在相对现代的架构中,这可以非常有效地完成 . 这个微小的开销是JIT时间的一部分,因此只发生一次 . 对于两个IL序列,生成的本机代码的质量是相同的 .

    因此,9字节序列的大小问题可能会导致任何数量的开销从无到有(假设我们在任何地方都使用它)并且3字节序列具有一次性的微小转换开销 . 哪一个更好?好吧,有人必须做一些科学合理的实验来衡量表现的差异来回答这个问题 . 我想强调,除非您是编译器优化的工程师或研究员,否则您不应该关心这一点 . 否则,您应该在更高级别(源代码级别)优化代码 .

  • 0

    因为float不是一个较小的double,而integer不是float(反之亦然) .

    所有 int 值在 long 值上都具有1:1映射 . 对于 floatdouble 来说,情况并非如此 - 浮点运算在这方面很棘手 . 更不用说int-float转换不是免费的 - 不像在栈中/寄存器中推送1字节值;看看这两种方法产生的x86-64代码,而不仅仅是IL代码 . IL代码的大小不是优化中要考虑的唯一因素 .

    这与 decimal 形成对比, decimal 实际上是基数为10的十进制数,而不是基数为2的十进制浮点数 . 20M 完美映射到 20 ,反之亦然,因此编译器可以自由发出:

    IL_0000:  ldc.i4.s    0A 
    IL_0002:  newobj      System.Decimal..ctor
    

    对于二进制浮点数,同样的方法根本不安全(或便宜!) .

    您可能认为这两种方法必然是安全的,因为在编译时我们是否从整数文字(“字符串”)转换为double值,或者我们是否在IL中进行转换并不重要 . 但事实并非如此,正如一些规范潜水揭晓:

    ECMA CLR规范,III.1.1.1:

    浮点数(静态,数组元素和类的字段)的存储位置具有固定大小 . 支持的存储大小为float32和float64 . 其他地方(在评估堆栈上,作为参数,作为返回类型和作为局部变量)浮点数使用内部浮点类型表示 . 在每个这样的例子中,名义类型变量或表达式是float32或float64,但其值可能在内部用额外的范围和/或精度表示 .

    为了简单起见,让我们假装float64实际使用4个二进制数字,而实现定义浮点类型(F)使用5个二进制数字 . 我们想要转换一个恰好具有超过四位数的二进制表示的整数文字 . 现在比较一下它的表现方式:

    ldc.r8 0.1011E2 ; expanded to 0.10110E2
    ldc.r8 0.1E2
    mul             ; 0.10110E2 * 0.10000E2 == 0.10110E3
    

    conv.r8 转换为F,而不是float64 . 所以我们实际得到:

    ldc.i4.s theSameLiteral
    conv.r8 ; converted to 0.10111E2
    mul     ; 0.10111E2 * 0.10000E2 == 0.10111E3
    

    哎呀:)

    现在,在任何合理的平台上,我都会在0-255的整数范围内发生这种情况 . 但是,因为我们做出了这样的假设 . JIT编译器可以,但's too late. The language compiler may define the two to be equivalent, but the C# specification doesn' t - double local被认为是float64,而不是F.如果您愿意,可以使用自己的语言 .

    在任何情况下,IL生成器都没有真正优化 . 这大部分留给了JIT编译 . 如果你想要一个优化的C#-IL编译器,写一个 - 我怀疑有足够的好处来保证努力,特别是如果你唯一的目标是使IL代码更小 . 大多数IL二进制文件已经比等效的本机代码小很多 .

    至于运行的实际代码,在我的机器上,两种方法都会产生完全相同的x86-64程序集 - 从数据段加载双精度值 . JIT可以轻松地进行优化,因为它知道代码实际运行的体系结构 .

相关问题