快速方法可以将整数乘以适当的分数,而无需浮点或溢出
Fast method to multiply integer by proper fraction without floats or overflow
我的程序经常需要执行以下计算:
给定:
- 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位方法或精确度损失巨大,因此不值得。
。显然,不能保证其他任何人都会在其他平台上获得类似的结果!
编辑:用普通代码替换一些略微划分的黑客,无论如何通过让编译器完成工作。
- 为不同配置设置MSVC_RUNTIME_LIBRARY的正确方法是什么
- 通过方法访问结构
- 最小硬币更换问题(自上而下方法)
- C++为构建时间获取QDateTime的可靠方法
- 在C#中处理C++指针而不使用unsafe的最佳方法
- 处理多个异常集合的C++方法
- 如果C++类在类方法中具有动态分配,但没有构造函数/析构函数或任何非静态成员,那么它仍然是POD类型吗
- 有什么方法可以遍历结构吗
- 当类在C++中定义时,有什么方法可以"register"类吗?
- 有没有一种方法可以捕获进程中的堆栈溢出?C++Linux
- 快速方法可以将整数乘以适当的分数,而无需浮点或溢出
- Ncurses/C/C++:使用 getstr() 并防止溢出(必须有更好的方法来做到这一点)
- 从字符串中读取溢出双倍作为'inf'的安全方法
- 为什么 C 和 C++ 没有内置方法来检查整数溢出?
- 有没有一种安全的方法可以在不触发溢出的情况下获得有符号整数的无符号绝对值
- 为什么在此方法中添加cout行可以阻止我的程序在Windows中触发缓冲区溢出异常?
- 在c++中检测缓冲区溢出源的最佳方法是什么?
- 在C/ c++中没有有效和可靠的方法来检测整数溢出
- 在乘法运算中测试有符号整数溢出的方法
- 二进制树插入方法导致堆栈溢出