【问题标题】:Detecting overflow in fixed-point multiplication检测定点乘法中的溢出
【发布时间】:2012-12-26 06:08:52
【问题描述】:

简短版:如何使用here 中描述的定点乘法检测溢出,但对于有符号类型?

长版:

我的Q31.32 fixed point type 仍然存在一些溢出问题。为了更容易在纸上编写示例,我使用相同的算法制作了一个小得多的类型,即基于 sbyte 的 Q3.4。我认为,如果我能解决 Q3.4 类型的所有问题,那么同样的逻辑应该适用于 Q31.32 类型。

请注意,我可以通过在 16 位整数上执行 Q3.4 乘法来非常轻松地实现它,但我这样做好像不存在,因为对于 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,因为除了hihi之外所有项都是0,但最终总和只使用了hihi的最低4位。

我通常的溢出检测魔法在这里不起作用:结果为零,因此结果的符号没有意义(例如 0.0625 * -0.0625 == 0(向下舍入),0 是正数但操作数的符号不同) ; hihi 的高位也是 1111,即使没有溢出也经常发生。

基本上我不知道如何检测这里发生的溢出。有没有更通用的方法?

【问题讨论】:

    标签: c# math fixed-point


    【解决方案1】:

    您应该检查hihi 以查看它是否包含超出结果范围的任何相关位。您还可以将结果的最高位与hihi 中的相应位进行比较,以查看进位是否传播了那么远,如果传播了(即该位已更改),是否表示溢出(即该位在方向错误)。如果您使用补码表示法并单独处理符号位,所有这些可能会更容易表述。但在那种情况下,您的 -8 示例将毫无意义。

    看看你的例子,你有hihi = 0xf0

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

    所以在这种情况下,如果 hihi 单独没有溢出,那么前 5 位将全部相同,结果的符号将与 hihi 的符号匹配。这不是这里的情况。您可以使用

    进行检查
    if ((hihi & 0x08) * 0x1f != (hihi & 0xf8))
      handle_overflow();
    

    hihi 的进位可能最容易检测到,方法是一次将结果相加,并在每一步之后执行常见的溢出检测。还没有准备好一段很好的代码。

    【讨论】:

    • 谢谢,但我最终自己弄清楚了一切。见我上面的回复。
    【解决方案2】:

    这花了我很长时间,但我最终想通了一切。此代码经过测试,可在 sbyte 允许的范围内适用于 x 和 y 的所有可能组合。这是注释代码:

        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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-05-21
      • 1970-01-01
      • 2012-01-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-10-19
      相关资源
      最近更新 更多