Эффективный способ округления чисел двойной точности до меньшей точности, заданной в количестве бит
В С# я хочу округлить удвоения до меньшей точности, чтобы я мог хранить их в ведрах разного размера в ассоциативном массиве. В отличие от обычного округления, я хочу округлить до нескольких значительных бит. Таким образом, большие числа будут меняться в абсолютном выражении гораздо больше, чем небольшие числа, но они будут иметь тенденцию к изменению одинаково пропорционально. Поэтому, если я хочу округлить до десяти двоичных цифр, я нахожу десять самых значащих бит и обнуляю все нижние биты, возможно добавляя небольшое число для округления.
Я предпочитаю округлять цифры "на полпути".
Если бы это был целочисленный тип, здесь был бы возможный алгоритм:
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)), если могу помочь.
Эта функция будет называться много миллионов раз.
Примечание. Я прочитал этот вопрос:
Каков хороший способ округлить значения двойной точности до (несколько) более низкой точности?
Он работает для С++. Я использую С#.
UPDATE:
Это код (измененный из того, что предоставил ответчик), когда я его использую:
/// <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 (с)
Ответы
Ответ 1
Алгоритм Деккерса разделит число с плавающей запятой на высокие и низкие части. Если в значении s есть бит s (53 в 64-разрядном двоичном коде IEEE 754), то *x0
получает биты с высоким s- b, которые вы запросили, а *x1
получает оставшиеся биты, которые вы можете отменить. В приведенном ниже коде Scale
должно иметь значение 2 b. Если b известно во время компиляции, например, константу 43, вы можете заменить Scale
на 0x1p43
. В противном случае вы должны произвести 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;
}
Ответ 2
Интересно... никогда не слышал о необходимости этого, но я думаю, что вы можете "сделать это" через какой-то фанковый небезопасный код...
void Main()
{
// how many bits you want "saved"
var maxBits = 20;
// create a mask like 0x1111000 where # of 1 == maxBits
var shift = (sizeof(int) * 8) - maxBits;
var maxBitsMask = (0xffffffff >> shift) << shift;
// some floats
var floats = new []{ 1.04125f, 2.19412347f, 3.1415926f};
foreach (var f in floats)
{
var localf = f;
unsafe
{
// float -> fixed point (sorta)
int toInt = *(int*)(&localf);
// mask off your least-sig bits
var modInt = toInt & maxBitsMask;
// fixed point -> float (sorta)
localf = *(float*)(&modInt);
}
Console.WriteLine("Was {0}, now {1}", f, localf);
}
}
И с удвоениями:
void Main()
{
var maxBits = 50;
var shift = (sizeof(long) * 8) - maxBits;
var maxBitsMask = (0xffffffffffffffff >> shift) << shift;
var doubles = new []{ 1412.04125, 22.19412347, 3.1415926};
foreach (var d in doubles)
{
var local = d;
unsafe
{
var toLong = *(ulong*)(&local);
var modLong = toLong & maxBitsMask;
local = *(double*)(&modLong);
}
Console.WriteLine("Was {0}, now {1}", d, local);
}
}
Все... Я не понял.:)
Для полноты здесь используется Jeppe "небезопасный" подход:
void Main()
{
var maxBits = 50;
var shift = (sizeof(long) * 8) - maxBits;
var maxBitsMask = (long)((0xffffffffffffffff >> shift) << shift);
var doubles = new []{ 1412.04125, 22.19412347, 3.1415926};
foreach (var d in doubles)
{
var local = d;
var asLong = BitConverter.DoubleToInt64Bits(d);
var modLong = asLong & maxBitsMask;
local = BitConverter.Int64BitsToDouble(modLong);
Console.WriteLine("Was {0}, now {1}", d, local);
}
}