【问题标题】:How should commutative operator overloads be efficiently implemented in C#?在 C# 中应该如何有效地实现交换运算符重载?
【发布时间】:2020-07-09 02:59:43
【问题描述】:

假设我有一个类型 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);
}

只有一个重载,类似于new Vector3(1,2,3) * 1.5 的表达式将编译但1.5 * new Vector3(1,2,3) 不会。由于向量标量乘法是可交换的,我希望任一顺序都可以工作,因此我添加了另一个重载,其参数反转,只会调用原始重载:

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

这是正确的做事方式吗?是否应该将第二个重载实现为

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

相反?天真地我希望编译器优化“额外”调用,并尽可能使用第一个重载(或者可能用长重载的主体替换短重载的主体),但我不知道的行为无论哪种方式,C# 编译器都足够好。

我意识到,在许多情况下,这是一种与算法选择相形见绌的性能问题,但在某些情况下,压榨每一滴性能是至关重要的。 在性能关键的情况下,是否应将交换运算符重载实现为两个除了参数顺序之外都相同的重载,还是将一个委托给另一个委托同样有效?

【问题讨论】:

  • 您可以尝试使用Multiply(Vector3f v, double d) 方法,用[MethodImpl(MethodImplOptions.AggressiveInlining)] 装饰它,然后从两个运算符中调用此方法,并希望最好。它不会在 IL 中,但 JIT 可能会内联 Multiply() 代码。
  • 调用一个方法(从 *(d,v) 调用 *(v,d) )有开销,如果你试图“挤压每一滴性能”,内联是至关重要的。

标签: c# performance operator-overloading operators commutativity


【解决方案1】:

在这里您可以看到两种方法之间的区别。

请记住,这是 IL,而不是 JIT 优化后生成的最终汇编代码。

  1. “实现为两个相同的重载,除了参数的顺序”

本例生成的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. “或者将一个代表交给另一个代表是否同样有效?”:

所以在这里您可以看到从*(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

当然,执行的 IL 操作总数会增加,如果您想避免这种情况,您应该在两个运算符中执行相同的代码。

您也可以尝试使用Multiply(Vector3f v, double d) 方法,用[MethodImpl(MethodImplOptions.AggressiveInlining)] 装饰它,然后从两个运算符中调用此方法,-- 并希望最好。它不会在 IL 中,但 JIT 可能会内联 Multiply() 代码。

也许大师们对此会有更多的发言权。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-08-21
    • 1970-01-01
    相关资源
    最近更新 更多