Обнаружение переполнения при умножении с фиксированной точкой

Краткая версия: как я могу обнаружить переполнение с помощью умножения с фиксированной точкой, описанного здесь, но для подписанного типа?

Длинная версия:

У меня все еще есть некоторые проблемы с переполнением с моим типом фиксированной точки 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, что часто происходит даже при отсутствии переполнения.

По сути, я не знаю, как определить, что здесь произошло переполнение. Есть ли более общий метод?


person Asik    schedule 26.12.2012    source источник


Ответы (2)


Вы должны проверить hihi, чтобы увидеть, содержит ли он какие-либо релевантные биты за пределами диапазона результата. Вы также можете сравнить старший бит результата с соответствующим битом в hihi, чтобы увидеть, распространился ли перенос так далеко, и если да (т. е. бит изменился), указывает ли это на переполнение (т. е. бит изменился в неправильном направлении). ). Все это, вероятно, было бы легче сформулировать, если бы вы использовали нотацию дополнения и обрабатывали знаковые биты отдельно. Но в таком случае ваш пример с −8 будет бессмысленным.

Глядя на ваш пример, у вас есть hihi = 0xf0.

hihi   11110000
result     ±###.####

Таким образом, в этом случае, если бы не было переполнения только в hihi, то все первые 5 битов были бы одинаковыми, а знак результата соответствовал бы знаку hihi. Это не тот случай здесь. Вы можете проверить это, используя

if ((hihi & 0x08) * 0x1f != (hihi & 0xf8))
  handle_overflow();

Перенос в hihi, вероятно, можно легко обнаружить, добавляя результат по одному слагаемому за раз и выполняя обычное обнаружение переполнения после каждого шага. У меня нет хорошего куска кода для этого.

person MvG    schedule 02.01.2013
comment
Спасибо, но в итоге я сам со всем разобрался. Смотрите мой ответ выше. - person Asik; 02.01.2013

Это заняло у меня много времени, но в конце концов я все понял. Этот код протестирован для работы для всех возможных комбинаций x и y в диапазоне, разрешенном sbyte. Вот закомментированный код:

    static sbyte AddOverflowHelper(sbyte x, sbyte y, ref bool overflow) {
        var sum = (sbyte)(x + y);
        // x + y overflows if sign(x) ^ sign(y) != sign(sum)
        overflow |= ((x ^ y ^ sum) & sbyte.MinValue) != 0;
        return sum;
    }

    /// <summary>
    /// Multiplies two Fix8 numbers.
    /// Deals with overflow by saturation.
    /// </summary>
    public static Fix8 operator *(Fix8 x, Fix8 y) {
        // Using the cross-multiplication algorithm, for learning purposes.
        // It would be both trivial and much faster to use an Int16, but this technique
        // won't work for a Fix64, since there's no Int128 or equivalent (and BigInteger is too slow).

        sbyte xl = x.m_rawValue;
        sbyte yl = y.m_rawValue;

        byte xlo = (byte)(xl & 0x0F);
        sbyte xhi = (sbyte)(xl >> 4);
        byte ylo = (byte)(yl & 0x0F);
        sbyte yhi = (sbyte)(yl >> 4);

        byte lolo = (byte)(xlo * ylo);
        sbyte lohi = (sbyte)((sbyte)xlo * yhi);
        sbyte hilo = (sbyte)(xhi * (sbyte)ylo);
        sbyte hihi = (sbyte)(xhi * yhi);

        byte loResult = (byte)(lolo >> 4);
        sbyte midResult1 = lohi;
        sbyte midResult2 = hilo;
        sbyte hiResult = (sbyte)(hihi << 4);

        bool overflow = false;
        // Check for overflow at each step of the sum, if it happens overflow will be true
        sbyte sum = AddOverflowHelper((sbyte)loResult, midResult1, ref overflow);
        sum = AddOverflowHelper(sum, midResult2, ref overflow);
        sum = AddOverflowHelper(sum, hiResult, ref overflow);

        bool opSignsEqual = ((xl ^ yl) & sbyte.MinValue) == 0;

        // if signs of operands are equal and sign of result is negative,
        // then multiplication overflowed positively
        // the reverse is also true
        if (opSignsEqual) {
            if (sum < 0 || (overflow && xl > 0)) {
                return MaxValue;
            }
        }
        else {
            if (sum > 0) {
                return MinValue;
            }
            // If signs differ, both operands' magnitudes are greater than 1,
            // and the result is greater than the negative operand, then there was negative overflow.
            sbyte posOp, negOp;
            if (xl > yl) {
                posOp = xl;
                negOp = yl;
            }
            else {
                posOp = yl;
                negOp = xl;
            }
            if (sum > negOp && negOp < -(1 << 4) && posOp > (1 << 4)) {
                return MinValue;
            }
        }

        // if the top 4 bits of hihi (unused in the result) are neither all 0s nor 1s,
        // then this means the result overflowed.
        sbyte topCarry = (sbyte)(hihi >> 4);
        // -17 (-1.0625) is a problematic value which never causes overflow but messes up the carry bits
        if (topCarry != 0 && topCarry != -1 && xl != -17 && yl != -17) {
            return opSignsEqual ? MaxValue : MinValue;
        }

        // Round up if necessary, but don't overflow
        var lowCarry = (byte)(lolo << 4);
        if (lowCarry >= 0x80 && sum < sbyte.MaxValue) {
            ++sum;
        }

        return new Fix8(sum);
    }

Я объединяю все это в должным образом протестированную математическую библиотеку с фиксированной точкой для .NET, которая будет доступна здесь: https://github.com/asik/FixedMath.Net

person Asik    schedule 02.01.2013