我想知道为什么这个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 回答
我怀疑你会得到比“没有人认为有必要实施它”更令人满意的答案 .
事实是,他们可以获得的收益不会超过成本,例如:额外测试,
int
和float
之间的非平凡转换,而在ldc.i4.s
的情况下,'s not that much of a trouble. Also it'最好不要使用更多优化规则来破坏抖动 .如Roslyn source code所示,转换仅针对
long
进行 . 总而言之,完全可以为float
或double
添加此功能,但除了生成更短的CIL代码(在需要内联时很有用),并且当您想要使用浮点常量时,它将没有多大用处,通常实际使用浮点数(即不是整数) .首先,让我们考虑正确性 .
ldc.i4.s
可以处理-128到127之间的整数,所有这些都可以在float32
中精确表示 . 但是,CIL对某些存储位置使用名为F
的内部浮点类型 . ECMA-335标准在III.1.1.1中说:这意味着无论
F
是什么,都保证在F
中安全地表示任何float32
值 .我们得出结论,您提出的替代指令序列是正确的 . 现在的问题是:在性能方面是否更好?
要回答这个问题,让我们看看JIT编译器在看到两个代码序列时的作用 . 使用
ldc.r8 20
时,您引用的链接中给出的答案很好地解释了使用长指令的后果 .让我们考虑3字节序列:
我们可以在这里做出一个对任何优化JIT编译器都合理的假设 . 我们是补充格式,必须将其转换为
float32
格式(如上所述,它始终是安全的) . 在相对现代的架构中,这可以非常有效地完成 . 这个微小的开销是JIT时间的一部分,因此只发生一次 . 对于两个IL序列,生成的本机代码的质量是相同的 .因此,9字节序列的大小问题可能会导致任何数量的开销从无到有(假设我们在任何地方都使用它)并且3字节序列具有一次性的微小转换开销 . 哪一个更好?好吧,有人必须做一些科学合理的实验来衡量表现的差异来回答这个问题 . 我想强调,除非您是编译器优化的工程师或研究员,否则您不应该关心这一点 . 否则,您应该在更高级别(源代码级别)优化代码 .
因为float不是一个较小的double,而integer不是float(反之亦然) .
所有
int
值在long
值上都具有1:1映射 . 对于float
和double
来说,情况并非如此 - 浮点运算在这方面很棘手 . 更不用说int-float转换不是免费的 - 不像在栈中/寄存器中推送1字节值;看看这两种方法产生的x86-64代码,而不仅仅是IL代码 . IL代码的大小不是优化中要考虑的唯一因素 .这与
decimal
形成对比,decimal
实际上是基数为10的十进制数,而不是基数为2的十进制浮点数 .20M
完美映射到20
,反之亦然,因此编译器可以自由发出:对于二进制浮点数,同样的方法根本不安全(或便宜!) .
您可能认为这两种方法必然是安全的,因为在编译时我们是否从整数文字(“字符串”)转换为double值,或者我们是否在IL中进行转换并不重要 . 但事实并非如此,正如一些规范潜水揭晓:
ECMA CLR规范,III.1.1.1:
为了简单起见,让我们假装float64实际使用4个二进制数字,而实现定义浮点类型(F)使用5个二进制数字 . 我们想要转换一个恰好具有超过四位数的二进制表示的整数文字 . 现在比较一下它的表现方式:
conv.r8
转换为F,而不是float64 . 所以我们实际得到:哎呀:)
现在,在任何合理的平台上,我都会在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可以轻松地进行优化,因为它知道代码实际运行的体系结构 .