快速将整数乘以适当的分数而没有浮点或溢出的方法

Tar*_*ron 13 c++ math bit-manipulation integer-overflow integer-division

我的程序经常需要执行以下计算:

鉴于:

  • N是32位整数
  • D是32位整数
  • abs(N)<= abs(D)
  • D!= 0
  • X是任意值的32位整数

找:

  • X * N / D为四舍五入的整数,X缩放为N / D(即10 * 2/3 = 7)

显然我可以直接使用r=x*n/d,但经常会从中溢出x*n。如果我改为这样做,r=x*(n/d)则由于整数除法会除去小数部分,因此我只会得到0或x。然后有,r=x*(float(n)/d)但在这种情况下我不能使用浮点数。

精度会很高,但并不像速度和决定性功能那么关键(总是在给定相同输入的情况下返回相同的值)。

N和D当前已签名,但如果有帮助,我可以解决它们始终未签名的问题。

可以使用任何X值(以及N和D,只要N <= D)的泛型函数是理想的,因为此操作以各种不同的方式使用,但是我也有一个特殊的情况,其中X的值是已知的保持2的幂(准确地说是2048),并且加快特定的调用速度将是一个很大的帮助。

目前,我正在使用64位乘法和除法来完成此操作,以避免溢出(本质上是,int multByProperFraction(int x, int n, int d) { return (__int64)x * n / d; }但是有一些断言和多余的位数摆弄而不是舍入)。

不幸的是,我的探查器报告64位除法函数占用了过多的CPU(这是一个32位应用程序)。我尝试减少执行此计算的频率,但用尽了很多方法,因此,即使有可能,我也在尝试找出一种更快的方法。在X的常数为2048的特定情况下,我使用了移位而不是乘法,但这并没有太大帮助。

Tar*_*ron 1

我现在已经对几种可能的解决方案进行了基准测试,包括来自其他来源的奇怪/聪明的解决方案,例如组合 32 位 div & mod & add 或使用农民数学,以下是我的结论:

首先,如果您仅针对 Windows 并使用 VSC++,则只需使用 MulDiv()。它相当快(比在我的测试中直接使用 64 位变量更快),同时仍然准确并为您舍入结果。即使考虑到诸如 unsigned-only 和 N <= D 之类的限制,我也找不到任何更好的方法在 Windows 上使用 VSC++ 执行此类操作。

然而,就我而言,即使跨平台,拥有具有确定性结果的函数甚至比速度更重要。在我用作测试的另一个平台上,使用 32 位库时,64 位除法比 32 位慢得多,并且没有 MulDiv() 可供使用。该平台上的 64 位除法大约需要 32 位除法的 26 倍(但 64 位乘法与 32 位版本一样快......)。

因此,如果您有像我这样的情况,我将分享我得到的最佳结果,结果证明这只是 chux 答案的优化。

我将在下面分享的两种方法都使用以下函数(尽管特定于编译器的内在函数实际上仅有助于提高 Windows 中 MSVC 的速度):

inline u32 bitsRequired(u32 val)
{
    #ifdef _MSC_VER
        DWORD r = 0;
        _BitScanReverse(&r, val | 1);
        return r+1;
    #elif defined(__GNUC__) || defined(__clang__)
        return 32 - __builtin_clz(val | 1);
    #else
        int r = 1;
        while (val >>= 1) ++r;
        return r;
    #endif
}
Run Code Online (Sandbox Code Playgroud)

现在,如果 x 是一个 16 位或更小的常量,并且您可以预先计算所需的位,我发现此函数在速度和准确性方面取得了最佳结果:

u32 multConstByPropFrac(u32 x, u32 nMaxBits, u32 n, u32 d)
{
    //assert(nMaxBits == 32 - bitsRequired(x));
    //assert(n <= d);
    const int bitShift = bitsRequired(n) - nMaxBits;
    if( bitShift > 0 )
    {
        n >>= bitShift;
        d >>= bitShift;
    }

    // Remove the + d/2 part if don't need rounding
    return (x * n + d/2) / d;
}
Run Code Online (Sandbox Code Playgroud)

在具有慢速 64 位除法的平台上,上述函数的运行速度约为 16.75 倍return ((u64)x * n + d/2) / d;,平均准确度为 99.999981%(比较返回值与预期 x 范围的差异,即,当当使用大约一百万个随机输入进行测试时,x 是 2048 将是 100 - (1/2048 * 100) = 99.95% 准确度,其中大约一半通常是溢出。最坏情况下的准确率为 99.951172%。

对于一般用例,我从以下内容中找到了最佳结果(并且不需要限制 N <= D 来启动!):

u32 scaleToFraction(u32 x, u32 n, u32 d)
{
    u32 bits = bitsRequired(x);
    int bitShift = bits - 16;
    if( bitShift < 0 ) bitShift = 0;
    int sh = bitShift;
    x >>= bitShift;

    bits = bitsRequired(n);
    bitShift = bits - 16;
    if( bitShift < 0 ) bitShift = 0;
    sh += bitShift;
    n >>= bitShift;

    bits = bitsRequired(d);
    bitShift = bits - 16;
    if( bitShift < 0 ) bitShift = 0;
    sh -= bitShift;
    d >>= bitShift;

    // Remove the + d/2 part if don't need rounding
    u32 r = (x * n + d/2) / d;
    if( sh < 0 )
        r >>= (-sh);
    else //if( sh > 0 )
        r <<= sh;

    return r;
}
Run Code Online (Sandbox Code Playgroud)

在 64 位除法速度较慢的平台上,上述函数的运行速度比使用 64 位变量快约 18.5 倍,平均准确率为 99.999426%,最坏情况准确率为 99.947479%。

我能够通过扰乱移位来获得更快的速度或更高的精度,例如如果不是绝对必要的话,尝试不一直向下移位到 16 位,但任何速度的提高都会以精度方面的高昂代价为代价反之亦然。

我测试的其他方法都没有达到相同的速度或精度,大多数都比仅使用 64 位方法慢或精度损失巨大,因此不值得讨论。

显然,不能保证其他人会在其他平台上获得类似的结果!

编辑:用纯代码替换了一些麻烦的黑客,这些代码实际上通过让编译器完成其工作而运行得更快。