常量和编译时评估 - 为什么要更改此行为

Max*_*ich 7 c# compiler-construction inference compiler-optimization

如果你向Eric Lippert 转发大约13分钟的视频,他描述了对C#编译器所做的更改,该更改使得以下代码无效(显然在此代码编译之前包括.NET 2).

int y;
int x = 10;
if (x * 0 == 0)
    y = 123;

Console.Write(y);
Run Code Online (Sandbox Code Playgroud)

现在我明白了上面代码的任何执行实际上都是为了评估

int y;
int x = 10;
y = 123;
Console.Write(y);
Run Code Online (Sandbox Code Playgroud)

但是我不明白为什么将下面的代码编译成"可编辑"被认为是"可取的"?IE:允许这样的推论运行的风险是什么?

Eri*_*ert 8

我仍然觉得这个问题有点令人困惑,但让我看看我是否可以将问题改写成我能回答的形式.首先,让我重新陈述问题的背景:

在C#2.0中,此代码:

int x = 123;
int y;
if (x * 0 == 0) 
    y = 345;
Console.WriteLine(y);
Run Code Online (Sandbox Code Playgroud)

被视为你写的

int x = 123;
int y;
if (true) 
    y = 345;
Console.WriteLine(y);
Run Code Online (Sandbox Code Playgroud)

反过来被视为:

int x = 123;
int y;
y = 345;
Console.WriteLine(y);
Run Code Online (Sandbox Code Playgroud)

这是一个合法的计划.

但在C#3.0中,我们采取了突破性的改变来防止这种情况发生.尽管你和我都知道它总是正确的,但编译器不再将条件视为"始终为真".我们现在将其设置为非法程序,因为编译器认为它不知道 "if"的主体总是被执行,因此不知道局部变量y在使用之前总是被赋值.

为什么C#3.0行为正确?

这是正确的,因为规范声明:

  • 常量表达式必须只包含常量.x * 0 == 0不是一个常量表达式,因为它包含一个非常数项,x.

  • if如果条件是等于的常量表达式,则只知道a 的结果总是可以到达true.

因此,给出的代码不应该将条件语句的结果分类为始终可访问,因此不应将本地分类y为明确分配.

为什么常量表达式只包含常量?

我们希望C#语言能够被用户清楚地理解,并且可以由编译器编写者正确实现.要求编译器对表达式的值进行所有可能的逻辑演绎,这些都是针对这些目标的.确定给定表达式是否为常量应该很简单,如果是,则其值是什么.简而言之,常量评估代码应该知道如何执行算术,但不应该知道关于算术操作的事实.常量赋值器知道如何乘以2*1,但它不需要知道"1是整数上的乘法同一性" 这一事实.

现在,编译器编写者可能会决定他们可以聪明的区域,从而生成更优的代码.允许编译器编写者这样做,但不能改变代码是合法的还是非法的.只有在给定合法代码时,才允许它们进行优化,使编译器的输出更好.

如何在C#2.0中发生错误?

发生的事情是编写器是为了过早地运行算术优化器而编写的.优化器应该是聪明的,它应该在程序被确定为合法之后运行.它在程序被确定为合法之前运行,因此影响了结果.

这是一个潜在的突破性变化:虽然它使编译器符合规范,但它也可能将工作代码转换为错误代码.是什么推动了变革?

LINQ功能,特别是表达式树.如果你说的话:

(int x)=>x * 0 == 0
Run Code Online (Sandbox Code Playgroud)

并将其转换为表达式树,您希望生成表达式树吗?

(int x)=>true
Run Code Online (Sandbox Code Playgroud)

?可能不是!您可能希望它生成"将x乘以零并将结果与​​零进行比较"的表达式树. 表达式树应该保留体内表达式的逻辑结构.

当我编写表达式树代码时,尚不清楚设计委员会是否会决定是否

()=>2 + 3
Run Code Online (Sandbox Code Playgroud)

将生成"添加二到三"的表达式树或"五"的表达式树.我们决定对后者-常量生成表达式树前折叠,但算术应该产生表达式树之前无法通过优化运行.

那么,现在让我们考虑一下我们刚才所说的依赖关系:

  • 算术优化必须在codegen之前进行.
  • 表达式树重写必须在算术优化之前进行
  • 在表达式树重写之前必须进行常量折叠
  • 在流动分析之前必须进行恒定折叠
  • 流分析必须在表达式树重写之前进行(因为我们需要知道表达式树是否使用未初始化的本地)

我们必须找到一个命令来完成所有这些工作,尊重所有这些依赖.C#2.0中的编译器按此顺序执行:

  • 同时进行常数折叠和算术优化
  • 流量分析
  • 代码生成

表达树重写在哪里?无处!显然这是错误的,因为流量分析现在考虑了算术优化器推导出的事实.我们决定重新编写编译器,以便它按顺序执行:

  • 不断折叠
  • 流量分析
  • 表达树重写
  • 算术优化
  • 代码生成

这显然需要改变.

现在,我确实考虑通过这样做来保留现有的破坏行为:

  • 不断折叠
  • 算术优化
  • 流量分析
  • 算术去优化
  • 表达树重写
  • 算术优化再次
  • 代码生成

优化的算术表达式将包含指向其未优化形式的指针.为了保护错误,我们认为这太复杂.我们决定最好修复bug,进行重大改变,并使编译器架构更容易理解.