Краткая версия: как я могу обнаружить переполнение с помощью умножения с фиксированной точкой, описанного здесь, но для подписанного типа?
Длинная версия:
У меня все еще есть некоторые проблемы с переполнением с моим типом фиксированной точки Q31.32. Чтобы было легче работать с примерами на бумаге, я сделал гораздо меньший шрифт, используя тот же алгоритм, Q3.4, основанный на sbyte. Я полагаю, что если я могу проработать все перегибы для типа Q3.4, та же логика должна применяться и для типа Q31.32.
Обратите внимание, что я мог бы очень легко реализовать умножение Q3.4, выполнив его для 16-битного целого числа, но я делаю так, как будто этого не существует, потому что для Q31.32 мне нужно 128-битное целое число, которое не существует (и BigInteger слишком медленный).
Я хочу, чтобы мое умножение обрабатывало переполнение по насыщению, то есть когда происходит переполнение, результатом является наибольшее или наименьшее значение, которое может быть представлено в зависимости от знака операндов.
В основном это то, как представлен тип:
struct Fix8 {
sbyte m_rawValue;
public static readonly Fix8 One = new Fix8(1 << 4);
public static readonly Fix8 MinValue = new Fix8(sbyte.MinValue);
public static readonly Fix8 MaxValue = new Fix8(sbyte.MaxValue);
Fix8(sbyte value) {
m_rawValue = value;
}
public static explicit operator decimal(Fix8 value) {
return (decimal)value.m_rawValue / One.m_rawValue;
}
public static explicit operator Fix8(decimal value) {
var nearestExact = Math.Round(value * 16m) * 0.0625m;
return new Fix8((sbyte)(nearestExact * One.m_rawValue));
}
}
И вот как я сейчас обрабатываю умножение:
public static Fix8 operator *(Fix8 x, Fix8 y) {
sbyte xl = x.m_rawValue;
sbyte yl = y.m_rawValue;
// split x and y into their highest and lowest 4 bits
byte xlo = (byte)(xl & 0x0F);
sbyte xhi = (sbyte)(xl >> 4);
byte ylo = (byte)(yl & 0x0F);
sbyte yhi = (sbyte)(yl >> 4);
// perform cross-multiplications
byte lolo = (byte)(xlo * ylo);
sbyte lohi = (sbyte)((sbyte)xlo * yhi);
sbyte hilo = (sbyte)(xhi * (sbyte)ylo);
sbyte hihi = (sbyte)(xhi * yhi);
// shift results as appropriate
byte loResult = (byte)(lolo >> 4);
sbyte midResult1 = lohi;
sbyte midResult2 = hilo;
sbyte hiResult = (sbyte)(hihi << 4);
// add everything
sbyte sum = (sbyte)((sbyte)loResult + midResult1 + midResult2 + hiResult);
// if the top 4 bits of hihi (unused in the result) are neither all 0s or 1s,
// then this means the result overflowed.
sbyte topCarry = (sbyte)(hihi >> 4);
bool opSignsEqual = ((xl ^ yl) & sbyte.MinValue) == 0;
if (topCarry != 0 && topCarry != -1) {
return opSignsEqual ? MaxValue : MinValue;
}
// if signs of operands are equal and sign of result is negative,
// then multiplication overflowed upwards
// the reverse is also true
if (opSignsEqual) {
if (sum < 0) {
return MaxValue;
}
}
else {
if (sum > 0) {
return MinValue;
}
}
return new Fix8(sum);
}
Это дает результат с точностью до точности типа и обрабатывает большинство случаев переполнения. Однако он не обрабатывает эти, например:
Failed -8 * 2 : expected -8 but got 0
Failed 3.5 * 5 : expected 7,9375 but got 1,5
Давайте разберемся, как происходит умножение для первого.
-8 and 2 are represented as x = 0x80 and y = 0x20.
xlo = 0x80 & 0x0F = 0x00
xhi = 0x80 >> 4 = 0xf8
ylo = 0x20 & 0x0F = 0x00
yhi = 0x20 >> 4 = 0x02
lolo = xlo * ylo = 0x00
lohi = xlo * yhi = 0x00
hilo = xhi * ylo = 0x00
hihi = xhi * yhi = 0xf0
Сумма, очевидно, равна 0, так как все члены равны 0, за исключением hihi, но в окончательной сумме используются только младшие 4 бита hihi.
Моя обычная магия обнаружения переполнения здесь не работает: результат равен нулю, поэтому знак результата не имеет значения (например, 0,0625 * -0,0625 == 0 (при округлении вниз), 0 положительный, но знаки операндов различаются); также старшие биты hihi равны 1111, что часто происходит даже при отсутствии переполнения.
По сути, я не знаю, как определить, что здесь произошло переполнение. Есть ли более общий метод?