Manera eficiente de redondear números de precisión doble a una precisión más baja dada en número de bits

En C #, quiero redondear los dobles a una precisión más baja para poder almacenarlos en grupos de diferentes tamaños en una matriz asociativa. A diferencia del redondeo habitual, quiero redondear a varios bits significativos. Por lo tanto, los números grandes cambiarán en términos absolutos mucho más que los números pequeños, pero tenderán a cambiar lo mismo proporcionalmente. Entonces, si quiero redondear a 10 dígitos binarios, encuentro los diez bits más significativos y pongo a cero todos los bits inferiores, posiblemente agregando un pequeño número para redondear hacia arriba.

Prefiero que los números de "medio camino" sean redondeados.

Si fuera un tipo entero, aquí sería un posible algoritmo:

  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.

El problema es encontrar una forma eficiente de encontrar el conjunto de bits más alto. Si estuviera usando números enteros, hay hacks de bits geniales para encontrar el MSB. No quiero llamar a Round (Log2 (x)) si puedo evitarlo. Esta función será llamada muchos millones de veces.

Nota: He leído esta pregunta SO:

¿Cuál es una buena manera de redondear los valores de precisión doble a una precisión (algo) más baja?

Funciona para C ++. Estoy usando C #.

ACTUALIZAR:

Este es el código (modificado de lo que proporcionó el respondedor) cuando lo estoy utilizando:

/// <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;
}

ACTUALIZACIÓN 2: Algoritmo de Dekker

Derivé esto del algoritmo de Dekker, gracias al otro encuestado. Se redondea al valor más cercano, en lugar de truncarse como lo hace el código anterior, y utiliza solo el código de seguridad:

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;
}

ACTUALIZACIÓN 2: TIEMPOS

¡Hice una prueba y descubrí que el algoritmo de Dekker es mejor que DOS VECES tan rápido!

Número de llamadas en prueba: 100,000,000
Tiempo inseguro = 1.922 (s)
Tiempo seguro = 0.799 (s)

Respuestas a la pregunta(2)

Su respuesta a la pregunta