为什么将短的 null 值转换为 int 类型的 null 值以与 null 进行比较?

40

比较可空的 short 类型值时,编译器会先将它们转换为整数再与空值进行比较。例如,考虑以下简单的代码:

short? cTestA;
if (cTestA == null) { ... }

它由编译器转换为:

short? CS$0$0001 = cTestA;
int? CS$0$0002 = CS$0$0001.HasValue ? new int?(CS$0$0001.GetValueOrDefault()) : null;
if (!CS$0$0002.HasValue){ ... }

这在所有 .NET 版本中都会发生,包括 .NET 4。

我漏掉了什么?为什么要进行两次转换才能检查 HasValue 值?

后续操作

我希望编译器进行简单的 .HasValue 检查,if (cTestA.HasValue){}。至少在我发现这个转换后,这是我在自己的代码中所做的。

为什么要为这样一个简单的测试添加所有这些额外的代码呢?

2个回答

36

关于你最新的更新:

这是可空算术优化器中的一个错误。

当你进行以下操作时,可空优化器会删除不必要的转换为 int?

short? s = null;
int? x = s + 1;

未经优化的代码生成器执行的相当于以下操作:
short? s = null;
int? x;
int? temp = s.HasValue ? new int?((int)s.Value) : new int?();
x = temp.HasValue ? new int?(x.Value + 1) : new int?();

优化后的代码生成器执行的是以下等效操作:
short? s = null;
int? x;
x = s.HasValue ? new int?((int)s.Value + 1) : new int?();

然而,优化器存在一个错误;我们没有删除等式中不必要的转换。
谢谢你提醒我,我们将在Roslyn中修复它。实际上,我将在接下来的几周内为Roslyn编写可空优化器。
更新:我确实编写了该优化器,如果你对它的工作原理感兴趣,我写了一系列文章介绍,从这里开始:

http://ericlippert.com/2012/12/20/nullable-micro-optimizations-part-one/


我想说的是,大多数情况下,我们使用从数据库获取的短数据,如果它为空,我们就这样处理,如果不为空,我们就调用其他数据库中的数据。没有算术操作,只是反映数据库数据的数据。它们很短,因为在数据库中很短以节省空间。类似于较小的类型。因此,仅读取一个short并进行一次测试然后使用它的代码是非常可能的。 - Aristos

29
请参见C# 4.0语言规范的4.1.5节。特别是,值得注意的是:
C#支持九种整数类型:sbyte、byte、short、ushort、int、uint、long、ulong和char。整数类型的一元和二元运算符始终使用带符号32位精度、无符号32位精度、带符号64位精度或无符号64位精度进行操作:
- 对于二进制+、-、*、/、%、&、^、|、==、!=、>、<、>=和<=运算符,操作数被转换为类型T,其中T是可以完全表示两个操作数所有可能值的int、uint、long和ulong中的第一个。然后使用类型T的精度执行操作,结果的类型为T(或关系运算符的bool类型)。不允许一个操作数是long类型,另一个操作数是ulong类型。 - 使用short进行的运算会提升为int,并且这些运算将被提升为它们的可空对应项。(这导致了第7.3.6.2节和第7.3.7节)
这是设计上的考虑,但仍然不理解为什么他们会这样做,他们已经优化了字符串的添加,为什么还要保留数字并添加更多代码来进行简单比较。
这只是语言设计的方式,考虑到现代架构的优化。虽然不是特别针对这个上下文,但请考虑Eric Lippert所说的话(here):
在C#中,算术永远不会使用shorts。算术可以在int、uint、long和ulong中完成,但算术永远不会使用shorts。shorts会提升为int,算术将在int中完成,因为如我之前所说,绝大多数算术计算都适用于int。绝大多数不适用于shorts。在现代硬件上,shorts算术可能会更慢,而且shorts算术不会占用更少的空间;它将在芯片上以int或long的形式完成。

您的最新更新:

我希望编译器至少使用.HasValue进行简单检查,例如(cTestA.HasValue),这是我在发现此转换后我的代码所做的。所以这就是我真正不明白为什么不做这个简单的事情,而要添加所有这些额外的代码。编译器总是试图优化代码-为什么在这里避免那个简单的.HasValue检查。我肯定错过了什么...

我必须请教编译器专家,以说明他们为什么选择进行转换而不是立即进行HasValue检查,除了说可能只是一个操作顺序。语言规范指出二进制运算符操作数会被提升,这就是提供的片段所做的。语言规范随后继续说,可以将对可空值类型x的检查x == null转换为!x.HasValue,这也是他们所做的。在您提供的编译代码中,数字推广仅仅优先于可空行为。

关于编译器总是试图优化代码的说法,专家可以澄清,但事实并非如此。编译器可以进行一些优化,而其他优化则可能由即时编译器处理。根据是否为调试或发布版本以及是否附加了调试器,编译器或即时编译器可能会做出不同的优化。毫无疑问,他们可能会进行一些他们选择不这样做的优化,因为成本与收益之间的平衡不成立。

好的,这是按设计要求+1的 - 但仍然不理解为什么他们要这样做,他们过度优化了字符串添加,为什么不把数字也一起优化,而要为这个简单的比较添加更多代码? - Aristos
我已经在随机复杂的代码中进行了检查,包括调试和发布模式。 - Aristos
2
@Aristos,我已经通过他的博客联系链接联系了Eric Lippert,希望他能回答你最新的“为什么”问题。在Stack Overflow参与者中,他几乎是唯一能够回答这些问题的人。 - Anthony Pegram

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