我想知道为什么这个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
的成本与代码大小的增加失衡?
答案 0 :(得分:7)
因为float不是较小的double,而integer不是float(反之亦然)。
所有int
值都在long
值上具有1:1映射。对float
和double
来说,同样的情况并非如此 - 浮点运算在这方面很棘手。更不用说int-float转换是不自由的 - 不像在堆栈上/寄存器中推送1字节值;看看这两种方法产生的x86-64代码,而不仅仅是IL代码。 IL代码的大小不是优化中唯一要考虑的因素。
这与decimal
形成对比,20M
实际上是基数为10的十进制数,而不是基数为2的十进制浮点数。 20
完全映射到IL_0000: ldc.i4.s 0A
IL_0002: newobj System.Decimal..ctor
,反之亦然,因此编译器可以自由发出:
ldc.r8 0.1011E2 ; expanded to 0.10110E2
ldc.r8 0.1E2
mul ; 0.10110E2 * 0.10000E2 == 0.10110E3
对于二进制浮点数,同样的方法根本不安全(或便宜!)。
您可能认为这两种方法必然是安全的,因为在编译时我们是否从整数文字("字符串")转换为double值并不重要。时间,或者我们是否在IL中这样做。但事实并非如此,正如一些规范潜水揭晓:
ECMA CLR规范,III.1.1.1:
浮点数(静态,数组元素和类的字段)的存储位置具有固定大小。支持的存储大小为float32和float64。 其他地方(在评估堆栈上,作为参数,作为返回类型和作为局部变量)浮点数使用内部浮点类型表示。在每个这样的实例中,变量或表达式的名义类型是float32或float64,但其值可能在内部用额外的范围和/或精度表示。
为了简单起见,让我们假装float64实际使用4个二进制数字,而实现定义的浮点类型(F)使用5个二进制数字。我们想要转换一个恰好具有超过四位数的二进制表示的整数文字。现在比较一下它的表现方式:
conv.r8
ldc.i4.s theSameLiteral
conv.r8 ; converted to 0.10111E2
mul ; 0.10111E2 * 0.10000E2 == 0.10111E3
转换为F,而不是float64。所以我们实际得到:
double
哎呀:)
现在,我非常确定在任何合理的平台上都不会出现0-255范围内的整数。但是,由于我们正在针对CLR规范进行编码,因此我们无法做出这一假设。 JIT编译器可以,但为时已晚。语言编译器可以将两者定义为等价,但C#规范并不是 - public class SendSMSActivity extends Activity {
Button buttonSend;
String smsBody = "Message from the API";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
buttonSend = (Button) findViewById(R.id.buttonSend);
textPhoneNo = (EditText) findViewById(R.id.editTextPhoneNo);
textSMS = (EditText) findViewById(R.id.editTextSMS);
final ArrayList <String> phone=new ArrayList<String>();
phone.add("9742504034");
phone.add("9535179695");
phone.add("9742504034");
phone.add("7204860021");
buttonSend.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
try {
for(int i = 0; i < phone.size(); i++)
{
SmsManager smsManager = SmsManager.getDefault();
smsManager.sendTextMessage(String.valueOf(phone), null, smsBody, null, null);
Toast.makeText(getApplicationContext(), "SMS Sent!",
Toast.LENGTH_LONG).show();
}
} catch (Exception e) {
Toast.makeText(getApplicationContext(),
"SMS faild, please try again later!",
Toast.LENGTH_LONG).show();
e.printStackTrace();
}
}
});
}
}
本地被认为是float64,而不是F.如果您愿意,可以使用自己的语言。
无论如何,IL生成器并没有真正优化。这大部分留给了JIT编译。如果你想要一个优化的C#-IL编译器,那就写一个 - 我怀疑是否有足够的好处来保证努力,特别是如果你的唯一目标是使IL代码更小。大多数IL二进制文件已经比同等的本机代码小了很多。
对于运行的实际代码,在我的机器上,两种方法都会产生完全相同的x86-64程序集 - 从数据段加载双精度值。 JIT可以轻松地进行优化,因为它知道代码实际运行的架构。
答案 1 :(得分:3)
我怀疑你会得到比#34更令人满意的答案;因为没有人认为有必要实施它。&#34;
事实是,他们可以通过这种方式实现这一目标,但正如Eric Lippert多次指出的那样,功能被选择实施而不是选择不实施。在这种特殊情况下,此功能的增益并不超过成本,例如额外的测试,int
和float
之间的非平凡转换,而在ldc.i4.s
的情况下,它并没有那么大的麻烦。此外,最好不要使用更多优化规则来消除抖动。
如Roslyn source code所示,转化仅针对long
进行。总而言之,完全可以为float
或double
添加此功能,但除了生成更短的CIL代码外,它不会有用(在内联时非常有用)当你想使用浮点常量时,你通常会使用浮点数(即不是整数)。
答案 2 :(得分:0)
首先,让我们考虑正确性。 ldc.i4.s
可以处理-128到127之间的整数,所有这些都可以在float32
中精确表示。但是,CIL对某些存储位置使用名为F
的内部浮点类型。 ECMA-335标准在III.1.1.1中说明:
...变量或表达式的名义类型是
float32
或float64
......内部代表应具有以下内容 特点:
- 内部表示的精度和范围应大于或等于标称类型。
- 来往内部代表的转换应保持价值。
这意味着,无论float32
是什么,都可以保证在F
中安全地表示任何F
值。
我们得出结论,您提出的替代指令序列是正确的。现在的问题是:在性能方面是否更好?
要回答这个问题,让我们看看JIT编译器在看到两个代码序列时的作用。使用ldc.r8 20
时,您引用的链接中给出的答案很好地解释了使用长指令的后果。
让我们考虑3字节序列:
ldc.i4.s 0x14
conv.r8
我们可以在此假设任何优化JIT编译器都是合理的。我们假设JIT能够识别这样的指令序列,以便可以将两个指令编译在一起。编译器被赋予以二进制补码格式表示的值0x14,并且必须将其转换为float32
格式(如上所述,它始终是安全的)。在相对现代的架构中,这可以非常有效地完成。这个微小的开销是JIT时间的一部分,因此只发生一次。两个IL序列的生成本机代码的质量相同。
因此,9字节序列的大小问题可能导致任何数量的开销从无到有(假设我们在任何地方使用它),并且3字节序列具有一次性的微小转换开销。哪一个更好?那么,有人必须做一些科学合理的实验来衡量表现的差异来回答这个问题。我想强调,除非您是编译器优化的工程师或研究员,否则您不应该关心这一点。否则,您应该在更高级别(源代码级别)优化代码。