C#编译器优化

4
为什么编译器要优化我的代码? 我有两个函数:
public void x1() {
  x++;
  x++;
}
public void x2() {
  x += 2;
}
public void x3() {
  x = x + 2;
}
public void y3() {
  x = x * x + x * x;
}

在发布模式下编译后,我可以使用ILSpy看到以下内容:

// test1.Something
public void x1()
{
    this.x++;
    this.x++;
}

// test1.Something
public void x2()
{
    this.x += 2;
}
// test1.Something
public void x3()
{ 
    this.x += 2;
}
// test1.Something
public void y3()
{
    this.x = this.x * this.x + this.x * this.x;
}

x2和x3可能可以,但为什么x1的优化结果不同呢?没有理由保持它是一个两步增量的形式。 而且为什么y3不是x=2*(x*x)?这难道不会比x*x+x*x更快吗?
这就引出一个问题:如果C#编译器不能处理这样简单的代码优化,那它到底能做些什么优化呢?
当我阅读关于编写代码的文章时,经常听到“代码要易读,编译器会完成其余工作”。但在这种情况下,编译器几乎什么也没做。
再来看一个例子:
public void x1() {
  int a = 1;
  int b = 1;
  int c = 1;
  x = a + b + c;
}

并使用ILSpy:

// test1.Something
public void x1()
{
    int a = 1;
    int b = 1;
    int c = 1;
    this.x = a + b + c;
}

为什么不是 this.x = 3?

1
什么让你相信 x=2*(x*x)x = x * x + x * x 更优化?可读性或术语的简化并不意味着公式操作更加优化,有时展开它反而更加优化。 - Ron Beyer
1
  1. 大多数优化是由JIT编译器在运行时执行的,而不是由C#编译器执行的。
  2. 对于x1您建议的优化在多线程面前并非功能等价(例如,如果该字段是volatile的,则编译器绝对禁止执行建议的优化)。
这是一篇确实有点老的文章,介绍了C#编译器执行的优化:http://blogs.msdn.com/b/ericlippert/archive/2009/06/11/what-does-the-optimize-switch-do.aspx
- Ani
少量的代码并不意味着更好的性能。 - Matias Cicero
另外,x++是一个特殊情况,因为它涉及缓存要返回的值,然后对其进行递增。 - Ron Beyer
两个乘法不比两个乘法和一个加法更快吗? - bebo
C#编译器也不是计算机代数系统,因此它无法执行任意代数操作以最小化操作数量。我总是对许多人似乎没有意识到这一点感到惊讶。 - Kyle
2个回答

6
编译器在不做出变量“x”与您的运行方法并发访问的假设的情况下无法执行此优化。否则,它会冒着以可检测的方式更改您的方法行为的风险。
考虑这样一种情况:当从两个线程同时访问“this”所引用的对象时。线程“A”重复将“x”设置为零;线程“B”重复调用“x1()”。
如果编译器将“x1”优化为等价于“x2”,则您的实验后x的两个可观察状态将分别为02
如果“A”在“B”之前完成,则得到2 如果“B”在“A”之前完成,则得到0 如果“A”在中间抢占了“B”,则仍然会得到2
但是,“x1”的原始版本允许三种结果:x最终可能是012
如果“A”在“B”之前完成,则得到2 如果“B”在“A”之前完成,则得到0 如果“B”在第一次递增后被抢占,然后“A”完成,然后“B”运行到完成,则得到1

4

x1x2是不同的:

如果x是一个公共字段并在多线程环境下被访问,那么第二个调用之间可能会有另一个线程改变x,而这不能再x2的代码中实现。

对于y2,如果x的类型重载了+和/或*,那么x*x + x*x可能与2*x*x不同。

编译器将优化以下内容(并非详尽列表):

  • 删除未使用的局部变量(释放寄存器)
  • 删除不影响逻辑流或输出的代码。
  • 内联简单方法的调用

编译器优化不应该改变程序的行为(尽管它确实发生)。所以重新排序/组合数学操作超出了优化范围。

写得易读,编译器会做剩下的工作。

好吧,编译器可能会做一些优化,但是在设计时间还有很多可以做来提高性能。是的,易读的代码绝对有价值,但编译器的任务是生成与源代码对应的工作IL,而不是更改源代码以使其运行更快。


1
我添加了一个与多线程无关的新示例。顺便说一下,如果编译器可以重新排列指令以优化性能,并且您需要采取特殊操作(如MemoryFences)来防止这种情况,那么为什么组合x ++和x ++不同呢?没有锁定或MemoryBarriear,我希望编译器不会关心多线程。 - bebo
1
我认为你对编译器的期望过高了。它的工作是将你的源代码转换成可运行的IL,而不是找到你的代码可能更快的每个潜在位置并重新编码。如果你觉得这是下一个C#编译器的有价值的特性,那就在http://connect.microsoft.com上发布它,看看会发生什么。 - D Stanley
但编译器的工作是生成与您的源代码相对应的可工作IL,而不是更改您的源代码以获得更快的速度。我认为优化器的目的是为了提高性能。 - bebo
1
为什么止步于此? 为什么不将您的“列表”更改为“数组”? 为什么不将您的一系列“if”语句更改为“switch”? 或者如果条件永远不可能为真,则完全删除一个“if”块? 这是很多逻辑要构建到编译器中,而许多人会抱怨它已经不够快了。 - D Stanley
1
我们问题的基本答案是:任何功能都必须经过设计、编码、测试、文档编写和支持,这一切都需要时间和金钱,而许多其他功能也在竞争中。如果这样一个功能的收益无法超过成本(或者其他功能提供更大的收益),那么它就不会被开发出来。我猜测这样一个优化功能的收益是微不足道的。 - D Stanley
1
@bebo编译器生成IL代码,但并不尝试进行优化。这是JIT的工作,通过对平台进行相关优化。有可能一些体系结构能够更快地执行“x ++; x ++;”,而其他体系结构能够更快地执行“x + = 2;”。话虽如此,我怀疑JIT会执行此特定的优化。因为在真实代码中这并不常见。顺便说一下,编译器不会内联调用,但JIT肯定会做到。 - Lucas Trzesniewski

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接