我正在开发一个数学库,我希望能够将其转换为使用
INumber<T>
中的新 System.Numerics
界面。这里的方法通常位于热路径上,因此如果它们能够尽可能快,同时使用起来也很愉快,那就太好了。我一直在对它们进行基准测试,并注意到一些看起来有点奇怪的事情。
在某些情况下,我们将数组值除以其索引,由于使用
System.Numerics
,这不能像定义类型时那样通过隐式转换来完成,推荐的方法似乎是使用INumber<T>.Create{Checked|Saturating|Truncating}(int n)
我正在运行的基准测试方法相当简单,如下所示:
public static double[] DivideByImplicit(double[] values)
{
double[] result = new double[values.Length];
for (int i = 1; i < result.Length; i++)
{
result[i] = values[i] / i;
}
return result;
}
// Overloads for other types (int, long, float, decimal)
// ...
public static T[] DivideByChecked<T>(T[] values) where T : INumber<T>
{
T[] result = new T[values.Length];
for (int i = 1; i < values.Length; i++)
{
result[i] = values[i] / T.CreateChecked(i);
}
return result;
}
// Same again but for Saturating/Truncating
// ...
对于
int
、long
和 decimal
,结果最多在几个百分点之内,但是当涉及 float
和 double
时,性能受到更大影响,双倍约为 25%,但浮动时间最多增加 100%:
方法 | 类别 | 数 | 意思是 | 错误 | 标准偏差 | 比率 | 比率SD |
---|---|---|---|---|---|---|---|
除以隐式小数 | 十进制 | 1000 | 29,292.733 纳秒 | 146.0540 纳秒 | 136.6190 纳秒 | 1.00 | 0.00 |
除以已检查的小数 | 十进制 | 1000 | 28,884.550 纳秒 | 82.0562 纳秒 | 76.7554 纳秒 | 0.99 | 0.00 |
除以饱和小数 | 十进制 | 1000 | 29,948.023 纳秒 | 61.6380 纳秒 | 54.6404 纳秒 | 1.02 | 0.01 |
除以截断小数 | 十进制 | 1000 | 30,367.067 纳秒 | 67.0287 纳秒 | 62.6987 纳秒 | 1.04 | 0.01 |
除以隐式双精度数 | 双 | 1000 | 1,067.421 纳秒 | 20.5178 纳秒 | 24.4250 纳秒 | 1.00 | 0.00 |
除以已检查双数 | 双 | 1000 | 1,342.562 纳秒 | 17.7660 纳秒 | 14.8354 纳秒 | 1.25 | 0.03 |
除以饱和双倍 | 双 | 1000 | 1,343.400 纳秒 | 16.4938 纳秒 | 13.7731 纳秒 | 1.25 | 0.04 |
除以截断双精度 | 双 | 1000 | 1,394.057 纳秒 | 27.2936 纳秒 | 44.0740 纳秒 | 1.31 | 0.04 |
除以隐式浮点数 | 漂浮 | 1000 | 636.576 纳秒 | 4.9312 纳秒 | 4.6127 纳秒 | 1.00 | 0.00 |
除以CheckedFloat | 漂浮 | 1000 | 1,282.109 纳秒 | 13.9466 纳秒 | 12.3633 纳秒 | 2.01 | 0.02 |
除以饱和浮点数 | 漂浮 | 1000 | 1,288.927 纳秒 | 10.4365 纳秒 | 8.7149 纳秒 | 2.03 | 0.02 |
通过截断浮点除法 | 漂浮 | 1000 | 1,291.335 纳秒 | 20.6446 纳秒 | 17.2392 纳秒 | 2.03 | 0.04 |
除以隐式整数 | int | 1000 | 1,210.849 纳秒 | 13.0819 纳秒 | 12.2368 纳秒 | 1.00 | 0.00 |
除以CheckedInt | int | 1000 | 1,202.773 纳秒 | 7.3879 纳秒 | 6.5492 纳秒 | 0.99 | 0.01 |
除以饱和整数 | int | 1000 | 1,201.430 纳秒 | 8.0413纳秒 | 6.7149 纳秒 | 0.99 | 0.01 |
除以截断整数 | int | 1000 | 1,199.158 纳秒 | 9.0592 纳秒 | 7.0728 纳秒 | 0.99 | 0.01 |
除以隐式长 | 长 | 1000 | 1,712.002 纳秒 | 19.3905 纳秒 | 17.1891 纳秒 | 1.00 | 0.00 |
除以检查长 | 长 | 1000 | 1,716.886 纳秒 | 17.9844 纳秒 | 16.8226 纳秒 | 1.00 | 0.01 |
除以饱和长 | 长 | 1000 | 1,712.213 纳秒 | 31.2334 纳秒 | 29.2157 纳秒 | 1.00 | 0.02 |
通过截断长整除 | 长 | 1000 | 1,771.120 纳秒 | 34.2546 纳秒 | 40.7777 纳秒 | 1.03 | 0.03 |
为什么转换为
float
/double
时的影响比转换为任何其他数字类型时的影响要高得多?为简洁起见,未显示,但我还对大小为 1、100 和 1_000_000 的数组运行了基准测试,并且在 1 或 100 上没有看到减速,但在 1_000_000 上类似。
CreateChecked
可能会很慢。您可以使循环使用另一个类型为 T
的循环变量,并将其用于除法。
public static T[] DivideByChecked<T>(T[] values) where T : INumber<T>
{
T[] result = new T[values.Length];
var tI = T.MultiplicativeIdentity;
for (int i = 1; i < values.Length; i++)
{
result[i] = values[i] / tI;
tI++;
}
return result;
}