快速方法可以将整数乘以适当的分数,而无需浮点或溢出

Fast method to multiply integer by proper fraction without floats or overflow

本文关键字:溢出 方法 整数      更新时间:2023-10-16

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

给定:

  • 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目前已签署,但如果有帮助,我可以始终不签名。

只要n< = d(与任何值的任何值一起使用的通用函数是理想的选择X是2个已知的常数功率(确切地说是2048年(,并且仅接到特定的电话将是一个很大的帮助。

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

不幸的是,我的profiler报告了64位分隔函数的占用过多的CPU(这是一个32位应用程序(。我试图减少需要进行此计算的频率,但要花所有的方法,因此,如果可能的话,我试图弄清楚一种更快的方法。在x是常数2048的具体情况下,我使用一些偏移而不是乘法,但这无济于事。

可耐受不重点并使用n,d,x的16个msbits

Algorithm
while (|n| > 0xffff) n/2, sh++
while (|x| > 0xffff) x/2, sh++
while (|d| > 0xffff) d/2, sh--
r = n*x/d  // A 16x16 to 32 multiply followed by a 32/16-bit divide.
shift r by sh.

64 bit鸿沟很昂贵时,这里的前/后处理可能值得进行32位鸿沟 - 这肯定是CPU的很大一部分。

如果编译器无法哄骗进行32位/16位划分,请跳过while (|d| > 0xffff) d/2, sh--步骤,然后进行32/32分隔。

尽可能使用无符号数学。

基本的正确方法就是(uint64_t)x*n/d。假设d是可变且无法预测的,那是最佳选择。但是,如果d是恒定的或很少发生变化,则可以预先生成常数,以便可以通过d进行精确的除法作为乘法,然后进行bitshift。在这里,GCC内部用来通过常数转换为乘法的算法的良好描述是:

http://ridiculousfish.com/blog/posts/labor-of-division-episode-iii.html

我不确定使它适用于" 64/32"部门(即分配(uint64_t)x*n的结果(有多容易,但是如果什么都没有,您应该可以将其分解为高和低零件否。

请注意,这些算法也可作为libdivide提供。

我现在已经基准了几种可能的解决方案,包括来自其他来源的怪异/聪明的解决方案,例如组合32位Div&amp&mod&添加或使用农民数学,这是我的结论:

首先,如果您仅针对Windows并使用VSC ,则只需使用Muldiv((即可。它的速度非常快(比直接在我的测试中使用64位变量直接使用(,同时仍然准确并为您填补结果。我找不到使用VSC 在Windows上执行此类操作的任何卓越方法,甚至考虑到无签名和N< = d。

等限制

但是,在我的情况下,即使在平台之间都具有确定性结果的功能,甚至比速度更重要。在我用作测试的另一个平台上,使用32位库时的64位鸿沟比32位的鸿沟要慢得多,并且没有使用muldiv((可以使用。该平台上的64位鸿沟只需32位划分(但是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
}

现在,如果x是大小或更小的常数,您可以预先计算所需的位,我从此功能中找到了速度和准确性的最佳结果:

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;
}

在平台上具有慢速64位划分的平台上,上述函数延伸〜16.75倍,速度为return ((u64)x * n + d/2) / d;,平均为99.999981%的准确性(比较从预期范围为x的返回值的差异,即返回/-返回/--1当x为2048时,预期的是100-(1/2048 * 100(= 99.95%精确量(在用一百万个左右的随机输入进行测试时,其中大约一半的输入通常是溢出。最差的准确性为99.951172%。

对于一般用例,我从以下内容中找到了最佳结果(并且不需要限制n&lt; = 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;
}

在平台上具有慢速64位划分的平台上,上述函数的运行〜18.5倍与使用64位变量和平均99.999426%和99.947479%最差的案例准确性一样快。

99.999426%。

我能够通过搞砸转移来获得更高的速度或更准确性,例如,如果不是严格必要的话,试图不将其一直转移到16位,但是速度的任何提高都达到了高度准确性成本,反之亦然。

我测试过的其他方法都没有接近相同的速度或准确性,大多数都比仅使用64位方法或精确度损失巨大,因此不值得。

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

编辑:用普通代码替换一些略微划分的黑客,无论如何通过让编译器完成工作。