Эффективный способ округления чисел с двойной точностью до более низкой точности, заданной числом бит

В 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.

Проблема в том, чтобы найти эффективный способ найти самый высокий набор битов. Если бы я использовал целые числа, есть классные хаки, чтобы найти MSB. Я не хочу звонить Раунду (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;
}

ОБНОВЛЕНИЕ 2: алгоритм Деккера

Я получил это из алгоритма Деккера, благодаря другому респонденту. Он округляется до ближайшего значения вместо усечения, как в приведенном выше коде, и использует только безопасный код:

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: СРОКИ

Я запустил тест и обнаружил, что алгоритм Деккера лучше, чем ДВАЖДЫ, так быстро!

Количество вызовов в тесте: 100 000 000
Небезопасное время = 1,922 (сек)
Безопасное время = 0,799 (сек)

Ответы на вопрос(2)

Ваш ответ на вопрос