应该如何在 C# 中有效地实现交换运算符重载?
How should commutative operator overloads be efficiently implemented in C#?
假设我有一个类型 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);
相反?天真地我希望编译器优化 "extra" 调用并尽可能使用第一个重载(或者可能用长重载的主体替换短重载的主体),但我不知道C# 编译器的行为足以说明任何一种方式。
我意识到,在很多情况下,这是一种与算法选择相形见绌的性能问题,但在某些情况下,压榨每一滴性能是至关重要的。 在性能关键的情况下,是否应该将可交换运算符重载实现为除了参数顺序之外完全相同的两个重载,还是将一个委托委托给另一个是否同样有效?
在这里您可以看到两种方法之间的区别。
请记住这是 IL 而不是 JIT 优化后生成的最终汇编代码。
- "implemented as two overloads that are identical except for the order of the parameters"
本例生成的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
- "or is it just as efficient to have one delegate to the other?":
所以在这里您可以看到从 *(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() 代码。
也许大师们会对此有更多的发言权。
假设我有一个类型 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);
相反?天真地我希望编译器优化 "extra" 调用并尽可能使用第一个重载(或者可能用长重载的主体替换短重载的主体),但我不知道C# 编译器的行为足以说明任何一种方式。
我意识到,在很多情况下,这是一种与算法选择相形见绌的性能问题,但在某些情况下,压榨每一滴性能是至关重要的。 在性能关键的情况下,是否应该将可交换运算符重载实现为除了参数顺序之外完全相同的两个重载,还是将一个委托委托给另一个是否同样有效?
在这里您可以看到两种方法之间的区别。
请记住这是 IL 而不是 JIT 优化后生成的最终汇编代码。
- "implemented as two overloads that are identical except for the order of the parameters"
本例生成的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
- "or is it just as efficient to have one delegate to the other?":
所以在这里您可以看到从 *(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() 代码。
也许大师们会对此有更多的发言权。