在C#中,我想将double类型的数字向下舍入到较低的精度,以便将它们存储在大小不同的桶中,并放入关联数组中。与通常的四舍五入不同,我想要保留指定数量的有效位数进行舍入。因此,大的数字在绝对意义上的变化比小的数字更大,但它们倾向于按相同的比例变化。因此,如果我想将数字舍入到10个二进制位,则找到最重要的十个位,并将所有较低的位清零,可能会添加一个小的数字来进行向上舍入。
我倾向于将"中间值"向上舍入。
如果这是整数类型,以下是可能的算法:
注意:我已阅读了这个SO问题: 什么是将双精度值舍入为(有些)较低精度的好方法? 它适用于C++。我正在使用C#。
更新:
这是代码(修改自答案提供者提供的代码),我正在使用它:
我倾向于将"中间值"向上舍入。
如果这是整数类型,以下是可能的算法:
1. Find: zero-based index of the most significant binary digit set H.
2. Compute: B = H - P,
where P is the number of significant digits of precision to round
and B is the binary digit to start rounding, where B = 0 is the ones place,
B = 1 is the twos place, etc.
3. Add: x = x + 2^B
This will force a carry if necessary (we round halfway values up).
4. Zero out: x = x mod 2^(B+1).
This clears the B place and all lower digits.
问题在于找到一种高效的方法来找到最高位设定。如果我使用整数,有很酷的位操作技巧可以找到MSB。如果可以避免,我不想调用Round(Log2(x))。该函数将被调用数百万次。注意:我已阅读了这个SO问题: 什么是将双精度值舍入为(有些)较低精度的好方法? 它适用于C++。我正在使用C#。
更新:
这是代码(修改自答案提供者提供的代码),我正在使用它:
/// <summary>
/// Round numbers to a specified number of significant binary digits.
///
/// For example, to 3 places, numbers from zero to seven are unchanged, because they only require 3 binary digits,
/// but larger numbers lose precision:
///
/// 8 1000 => 1000 8
/// 9 1001 => 1010 10
/// 10 1010 => 1010 10
/// 11 1011 => 1100 12
/// 12 1100 => 1100 12
/// 13 1101 => 1110 14
/// 14 1110 => 1110 14
/// 15 1111 =>10000 16
/// 16 10000 =>10000 16
///
/// This is different from rounding in that we are specifying the place where rounding occurs as the distance to the right
/// in binary digits from the highest bit set, not the distance to the left from the zero bit.
/// </summary>
/// <param name="d">Number to be rounded.</param>
/// <param name="digits">Number of binary digits of precision to preserve. </param>
public static double AdjustPrecision(this double d, int digits)
{
// TODO: Not sure if this will work for both normalized and denormalized doubles. Needs more research.
var shift = 53 - digits; // IEEE 754 doubles have 53 bits of significand, but one bit is "implied" and not stored.
ulong significandMask = (0xffffffffffffffffUL >> shift) << shift;
var local_d = d;
unsafe
{
// double -> fixed point (sorta)
ulong toLong = *(ulong*)(&local_d);
// mask off your least-sig bits
var modLong = toLong & significandMask;
// fixed point -> float (sorta)
local_d = *(double*)(&modLong);
}
return local_d;
}
更新2:Dekker算法
我从Dekker算法中推导出了这个算法,感谢其他回答者。它会四舍五入到最接近的值,而不是像上面的代码那样截断,并且只使用安全的代码:
private static double[] PowersOfTwoPlusOne;
static NumericalAlgorithms()
{
PowersOfTwoPlusOne = new double[54];
for (var i = 0; i < PowersOfTwoPlusOne.Length; i++)
{
if (i == 0)
PowersOfTwoPlusOne[i] = 1; // Special case.
else
{
long two_to_i_plus_one = (1L << i) + 1L;
PowersOfTwoPlusOne[i] = (double)two_to_i_plus_one;
}
}
}
public static double AdjustPrecisionSafely(this double d, int digits)
{
double t = d * PowersOfTwoPlusOne[53 - digits];
double adjusted = t - (t - d);
return adjusted;
}
更新2:时间
我进行了一项测试,发现Dekker算法比TWICE快了一倍以上!
测试中的调用次数:100,000,000
不安全时间 = 1.922(秒)
安全时间 = 0.799(秒)
如何将双精度值舍入到(稍微)较低的精度?
之后,您在编写解决方案方面有什么想法。为什么不尝试一下所读内容,如果需要重构或优化,那么请在这里报告结果和/或错误。除非您已经有代码,否则请将其与原始问题一起发布。 - MethodMan