【问题标题】:How does C# handle operator overloadingC#如何处理运算符重载
【发布时间】:2012-02-29 14:19:40
【问题描述】:

假设我在 C# 中有一个复数类,它的加法运算符是这样定义的。

public static Complex operator +(Complex c1, Complex c2) 
 {
      return new Complex(c1.real + c2.real, c1.imaginary + c2.imaginary);
 }

并像这样使用。

 Complex c1, c2, c3, c4;

 c1 = new Complex(...)
 ...
 c4 = new Complex(...)

 Complex csum = c1 + c2 + c3 + c4;

现在,我的问题是 C# 编译器+运行时将如何处理这个问题。显然,它看起来会做这样的事情。

 ct1 = c1 + c2; // ct1 is a temporary object created by the compiler
 ct2 = ct1 + c3;
 csum = ct2 + c4;

或者它是否足够聪明地意识到它可以像这样以更好的方式(更少创建新的临时对象)来做到这一点。

 ct = c1 + c2;
 ct += c3;
 csum = ct + c4; 

【问题讨论】:

  • 这不是编译器的工作。 编译器编译时间有什么神奇之处,内存分配是在运行时——CLR/JIT工作...
  • gdoron,我在问编译器将如何为此生成代码,或者如果您愿意,编译器+运行时将如何处理。
  • 您的优化版本会忘记 c4 - 不过非常快!
  • 您可以使用 ildasm 查看编译器生成的内容。 @Eugen 我很确定我看到了同样的错误,一定是被编辑过的。
  • 出于好奇,您为什么想知道编译器将如何执行该操作?

标签: c# optimization operator-overloading


【解决方案1】:

除了有一个命名的局部变量之外,单个表达式与单独的语句等效(就生成的代码而言)。 JIT 可能会优化它,但编译器不能。请注意,如果Complex 是一个值类型,那么它实际上并不涉及分配对象1

值得注意的是,+ 运算符是专门为 C# 中的 string 处理的(它由语言指定;它不在框架本身中)正是为了避免这种事情:x + y + z 是(或在至少可以) 转换为string.Concat(x, y, z) 以避免创建临时字符串。


1 “对象”是指带有对象头(类型引用、同步块等)的内存区域,后跟在堆上分配的字段,而不是“值类型” value”,它只包括数据本身,可能在堆上或栈上。堆栈/堆部分当然是implementation detail,但可能很重要......我相信区分“只是一个值类型值”和“一个完整的对象”是合理的 .

【讨论】:

  • “并不真正涉及分配对象”:everything 是 .NET 中的一个对象。据我所知,+ 运算符特殊处理仅适用于字符串——字符串类甚至可能在 BCL 中 + 运算符。
  • @phoog:哎呀,我的意思是明确声明“for string”——这正是我要说的:)至于“一切都是对象”——我喜欢区分值类型值和一个对象(具有同步块等)。当然,拳击会把一种变成另一种。如果有明确定义的术语就好了,但我在任何地方都没有看到。
  • @phoog:添加了一个解释性脚注 - 看看你的想法。
  • 是的,如果有明确定义的术语,那就太好了——事实上,我发布评论更多的是为了引起人们对这一事实的关注,而不是出于任何其他原因。我认为“垃圾收集的引用类型对象”是准确的,但它不是一个很好的候选者,因为它既不方便也不简洁。
  • 我记得在 Microsoft 文档中看到过“垃圾收集堆”一词,而 GC 确实似乎是重要的区别之一。通过类比托管和非托管类型之间的区别(与指针相关),也许我们可以使用“托管对象”。
【解决方案2】:

关于编译输出,您推断的逐步过程都不正确。 CLR 是一个堆栈机器,由于一次将所有四个相加的代码并不表示需要将中间总计作为命名变量单独分配,因此它会在适当的位置更新运行总计(或者更确切地说,弹出并推送顶部堆栈元素)。

您的两个逐步版本都需要相同数量的实际操作;一元加法版本只需要少一个局部变量分配。

以下是通过将四个输入复数相加的三种不同方法生成的实际 IL。请注意,在我的版本中,我将 Complex 类(我假设您从 How to: Use Operator Overloading to Create a Complex Number Class (C# Programming Guide) 提升)更改为保存两个 double 值而不是两个 ints。为简洁起见,我在 C# 和 IL 代码中都省略了 c1..c4 的声明。简而言之,两种推断的逐步方法都需要两个额外的stloc.s(将一个值推送到变量列表)和ldloc.s(从变量列表中检索一个值)调用。

原文:将所有四个连续添加:

C#:

Complex csum = c1 + c2 + c3 + c4;

IL:

// MultipleAdd : 5 Locals, maxstack 3
IL_0068:  nop
IL_0069:  ldloc.0
IL_006a:  ldloc.1
IL_006b:  call       valuetype Test.Complex Test.Complex::op_Addition(valuetype Test.Complex,
                                                                    valuetype Test.Complex)
IL_0070:  ldloc.2
IL_0071:  call       valuetype Test.Complex Test.Complex::op_Addition(valuetype Test.Complex,
                                                                    valuetype Test.Complex)
IL_0076:  ldloc.3
IL_0077:  call       valuetype Test.Complex Test.Complex::op_Addition(valuetype Test.Complex,
                                                                    valuetype Test.Complex)
IL_007c:  stloc.s    csum
IL_007e:  ret

推论 1:三个二进制加法

C#:

Complex ct1 = c1 + c2; 
Complex ct2 = ct1 + c3;  
Complex csum = ct2 + c4; 

IL:

// BinaryAdd : 7 Locals, maxstack 3
IL_0068:  nop
IL_0069:  ldloc.0
IL_006a:  ldloc.1
IL_006b:  call       valuetype Test.Complex Test.Complex::op_Addition(valuetype Test.Complex,
                                                                    valuetype Test.Complex)
IL_0070:  stloc.s    ct1
IL_0072:  ldloc.s    ct1
IL_0074:  ldloc.2
IL_0075:  call       valuetype Test.Complex Test.Complex::op_Addition(valuetype Test.Complex,
                                                                    valuetype Test.Complex)
IL_007a:  stloc.s    ct2
IL_007c:  ldloc.s    ct2
IL_007e:  ldloc.3
IL_007f:  call       valuetype Test.Complex Test.Complex::op_Addition(valuetype Test.Complex,
                                                                    valuetype Test.Complex)
IL_0084:  stloc.s    csum
IL_0086:  ret 

推理 2:一元加法

C#:

Complex ct = c1 + c2; 
ct += c3; 
Complex csum = ct + c4;

IL:

// UnaryAdd : 6 Locals, maxstack 3
IL_0068:  nop
IL_0069:  ldloc.0
IL_006a:  ldloc.1
IL_006b:  call       valuetype Test.Complex Test.Complex::op_Addition(valuetype Test.Complex,
                                                                    valuetype Test.Complex)
IL_0070:  stloc.s    ct
IL_0072:  ldloc.s    ct
IL_0074:  ldloc.2
IL_0075:  call       valuetype Test.Complex Test.Complex::op_Addition(valuetype Test.Complex,
                                                                    valuetype Test.Complex)
IL_007a:  stloc.s    ct
IL_007c:  ldloc.s    ct
IL_007e:  ldloc.3
IL_007f:  call       valuetype Test.Complex Test.Complex::op_Addition(valuetype Test.Complex,
                                                                    valuetype Test.Complex)
IL_0084:  stloc.s    csum
IL_0086:  ret

【讨论】:

  • 所以这意味着,C# 确实为每个加法操作创建一个新对象。对?还是像 C++ 移动语义一样重用同一个临时对象?
  • @MetallicPriest 这不是 C# 的问题,而是 CIL / MSIL 的问题。但这就是说我应该包含Complex::op_Addition 的IL:最后一步实际上是通过将最后两个值从堆栈中弹出来创建一个新的Complex 对象(newobj op) --realcomplex这两个输入字段相加的结果。
猜你喜欢
  • 2010-12-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多