0

假设我有一个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# 编译器足以说明任何一种方式。

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

4

1 回答 1

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. “或者让一个代表给另一个代表同样有效?”:

所以在这里你可以看到*(v,d)从操作符内部调用操作符*(d,v)的开销

.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() 代码。

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

于 2020-03-28T15:39:37.960 回答