简体   繁体   English

如何在 C# 中有效地实现可交换运算符重载?

[英]How should commutative operator overloads be efficiently implemented in C#?

Say I have a type Vector3 with an overloaded operator * allowing multiplication by a double:假设我有一个带有重载运算符 * 的类型Vector3允许乘以双精度:

public readonly struct Vector3
{
    public double X { get; }
    public double Y { get; }
    public double Z { get; }

    public Vector3f(double x, double y, double z)
    {
        X = x;
        Y = y;
        Z = z;
    }

    public static Vector3f operator *(in Vector3f v, in double d) => new Vector3f(d * v.X, d * v.Y, d * v.Z);
}

With only the one overload, expressions akin to new Vector3(1,2,3) * 1.5 will compile but 1.5 * new Vector3(1,2,3) will not.只有一个重载,类似于new Vector3(1,2,3) * 1.5表达式会编译,但1.5 * new Vector3(1,2,3)不会。 Since vector-scalar multiplication is commutative I would like either order to work, so I add another overload with the parameters reversed that will just call the original overload:由于向量标量乘法是可交换的,我希望任何一个顺序都能工作,所以我添加了另一个重载,参数颠倒,只会调用原始重载:

public static Vector3f operator *(in double d, in Vector3f v) => v * d;

Is that the correct way to do things?这是做事的正确方法吗? Should the second overload be implemented as第二个重载是否应该实现为

public static Vector3f operator *(in double d, in Vector3f v) => new Vector3f(d * v.X, d * v.Y, d * v.Z);

instead?反而? Naively I'd expect the compiler to optimise the "extra" call away and always use the first overload if possible (or maybe replace the body of the short overload with that of the long one), but I don't know the behaviour of the C# compiler well enough to say either way.天真地我希望编译器优化“额外”调用并尽可能使用第一个重载(或者可能用长重载的主体替换短重载的主体),但我不知道的行为无论哪种方式,C# 编译器都足够好。

I realise that in many cases this is the sort of performance quibble that is dwarfed by algorithm choice, but in some cases squeezing every last drop of performance is critical.我意识到在很多情况下,这是一种算法选择相形见绌的性能狡辩,但在某些情况下,挤压每一滴性能都是至关重要的。 In performance-critical cases, should commutative operator overloads be implemented as two overloads that are identical except for the order of the parameters, or is it just as efficient to have one delegate to the other?在性能关键的情况下,是否应该将可交换运算符重载实现为两个重载,除了参数的顺序之外,它们是相同的,还是将一个委托给另一个委托同样有效?

Here you can see the difference between the two approaches.在这里您可以看到两种方法之间的区别。

Please remember that this is IL and not the final assembly code generated after JIT optimizations.请记住,这是 IL 而不是 JIT 优化后生成的最终汇编代码。

  1. "implemented as two overloads that are identical except for the order of the parameters" “作为两个相同的重载实现,除了参数的顺序”

The generated IL in this case is below.在这种情况下生成的 IL 如下。

.method public hidebysig specialname static 
        valuetype lib.Vector3f  op_Multiply([in] float64& d,
                                            [in] valuetype lib.Vector3f& v) cil managed
{
  .param [1]
  .custom instance void System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = ( 01 00 00 00 ) 
  .param [2]
  .custom instance void System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = ( 01 00 00 00 ) 
  // Code size       33 (0x21)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  ldind.r8
  IL_0002:  ldarg.1
  IL_0003:  call       instance float64 lib.Vector3f::get_X()
  IL_0008:  mul
  IL_0009:  ldarg.0
  IL_000a:  ldind.r8
  IL_000b:  ldarg.1
  IL_000c:  call       instance float64 lib.Vector3f::get_Y()
  IL_0011:  mul
  IL_0012:  ldarg.0
  IL_0013:  ldind.r8
  IL_0014:  ldarg.1
  IL_0015:  call       instance float64 lib.Vector3f::get_Z()
  IL_001a:  mul
  IL_001b:  newobj     instance void lib.Vector3f::.ctor(float64,
                                                         float64,
                                                         float64)
  IL_0020:  ret
} // end of method Vector3f::op_Multiply
  1. "or is it just as efficient to have one delegate to the other?": “或者让一位代表交给另一位代表是否同样有效?”:

So here you can see the overhead of calling the *(v,d) operator from inside the *(d,v) operator所以在这里你可以看到从*(d,v)运算符内部调用*(v,d)运算符的开销

.method public hidebysig specialname static 
        valuetype lib.Vector3f  op_Multiply([in] float64& d,
                                            [in] valuetype lib.Vector3f& v) cil managed
{
  .param [1]
  .custom instance void System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = ( 01 00 00 00 ) 
  .param [2]
  .custom instance void System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = ( 01 00 00 00 ) 
  // Code size       8 (0x8)
  .maxstack  8
  IL_0000:  ldarg.1
  IL_0001:  ldarg.0
  IL_0002:  call       valuetype lib.Vector3f lib.Vector3f::op_Multiply(valuetype lib.Vector3f&,
                                                                        float64&)
  IL_0007:  ret
} // end of method Vector3f::op_Multiply

There is, of course, an increase in the total number of IL operations executed, and if this is what you want to avoid, you should have the same code executed in both of your operators.当然,执行的 IL 操作总数会增加,如果这是您想要避免的情况,您应该在两个操作符中执行相同的代码。

You can also try having a Multiply(Vector3f v, double d) method, decorate it with [MethodImpl(MethodImplOptions.AggressiveInlining)] and call this method from both operators, -- and hope for the best.您也可以尝试使用Multiply(Vector3f v, double d)方法,用[MethodImpl(MethodImplOptions.AggressiveInlining)]装饰它并从两个运算符调用此方法,并希望最好。 It will not be in the IL, but JIT will probably inline the Multiply() code.它不会在 IL 中,但 JIT 可能会内联 Multiply() 代码。

Maybe masters will have more to say on this.或许大师们对此会有更多的说法。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM