将双精度数舍入到以位数给出的较低精度的有效方法

Pau*_*och 6 c# floating-point rounding

在C#中,我希望将双精度舍入到较低的精度,以便我可以将它们存储在关联数组中的不同大小的存储桶中.与通常的舍入不同,我想要舍入到一些重要的位.因此,大数字的绝对值会比小数字更改,但它们往往会按比例改变.因此,如果我想要舍入到10个二进制数字,我会找到十个最高有效位,并将所有低位都清零,可能会添加一个小数字进行舍入.

我更喜欢将"中途"数字四舍五入.

如果它是整数类型,这里可能是一个算法:

  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.
Run Code Online (Sandbox Code Playgroud)

问题是找到找到最高位集的有效方法.如果我使用整数,那么找到MSB就会有很酷的攻击.如果我可以帮助它,我不想调用Round(Log2(x)).此功能将被调用数百万次.

注意:我已经阅读了这个问题:

什么是将双精度值舍入到(稍微)更低精度的好方法?

它适用于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;
}
Run Code Online (Sandbox Code Playgroud)

更新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;
}
Run Code Online (Sandbox Code Playgroud)

更新2:时间安排

我进行了测试,发现Dekker的算法比TWICE快得多!

测试中的呼叫数量:100,000,000
不安全时间= 1.922(秒)
安全时间= 0.799(秒)

Eri*_*hil 8

Dekker的算法将浮点数分成高低部分.如果有效数据中有s位(IEEE 754 64位二进制中有53位),则*x0接收您请求的高s - b位,并*x1接收剩余的位,您可以丢弃这些位.在下面的代码中,Scale应该具有值2 b.如果b在编译时已知,例如常数43,则可以替换Scale0x1p43.否则,你必须以某种方式产生2 b.

这需要圆到最近的模式.IEEE 754算术就足够了,但其他合理的算法也可以.它将关系变为偶数,这不是你要求的(向上绑定).这有必要吗?

这假定x * (Scale + 1)不会溢出.必须以双精度(不大于)精度评估操作.

void Split(double *x0, double *x1, double x)
{
    double d = x * (Scale + 1);
    double t = d - x;
    *x0 = d - t;
    *x1 = x - *x0;
}
Run Code Online (Sandbox Code Playgroud)