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

时间:2016-12-09 10:23:13

标签: c# .net compiler-optimization cil

我想知道为什么这个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 个答案:

答案 0 :(得分:7)

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

所有int值都在long值上具有1:1映射。对floatdouble来说,同样的情况并非如此 - 浮点运算在这方面很棘手。更不用说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多次指出的那样,功能被选择实施而不是选择不实施。在这种特殊情况下,此功能的增益并不超过成本,例如额外的测试,intfloat之间的非平凡转换,而在ldc.i4.s的情况下,它并没有那么大的麻烦。此外,最好不要使用更多优化规则来消除抖动。

Roslyn source code所示,转化仅针对long进行。总而言之,完全可以为floatdouble添加此功能,但除了生成更短的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字节序列具有一次性的微小转换开销。哪一个更好?那么,有人必须做一些科学合理的实验来衡量表现的差异来回答这个问题。我想强调,除非您是编译器优化的工程师或研究员,否则您不应该关心这一点。否则,您应该在更高级别(源代码级别)优化代码。