30

如果精度不是至关重要的,有没有办法提高速度的倒数(1除以X)?

所以,我需要计算1/X。有什么解决办法可以让我失去精度,但速度更快吗?

4
  • 这在很大程度上取决于您正在使用的硬件平台。此外,这还取决于您准备损失多少精度。显然,浮动往复(float x){return 1;}速度很快,但不太准确。。。 评论 2012年3月30日8:25
  • 10
    单精度倒数在最新的处理器上以5个周期运行。浮点乘法也是5个循环。所以我真的怀疑你会比这样的人更快(浮动)1/(浮动)x.
    – 神秘的
    评论 2012年3月30日8:25
  • 首先,您的平台和编译器是什么?你在操作什么样的数据? 评论 2012年3月30日13:01
  • @神秘小心5个周期是绝对最好的情况下最低的延迟,但另一个数字是最坏的情况下约37个周期?记住,硬件实现了迭代寻根近似算法,如牛顿法,直到精度足够 评论 2017年4月6日3:47

7个答案7

重置为默认值
15

𝗛𝗲𝗿𝗲'𝘀 𝗛𝗼𝘄 𝗧𝗼 𝗔𝗽𝗽𝗿𝗼𝘅𝗶𝗺𝗮𝘁𝗲 𝗠𝗼𝗿𝗲 𝗘𝗳𝗳𝗶𝗰𝗶𝗲𝗻𝘁𝗹𝘆

我相信他所寻找的是一种更有效的近似1.0/x的方法,而不是近似的一些技术定义,即可以使用1作为非常不精确的答案。我也相信这满足了这一点。

#ifdef __cplusplus#包括<cstdint>#其他#包括<stdint.h>#结尾__内联_双__attribute_((常数))倒数(双x){活接头{双dbl;#如果定义__cplusplus标准::uint_least64_tull;#其他uint_least64_tull;#结尾}u;u.dbl=x;u.ull=(0xbfcdd6a18f6a6f52ULL-u.ul)>>1;//功率(x,-0.5)u.dbl*=u.dbl;//功率(pow(x,-0.5),2)=功率(x,-1)=1.0/x返回u.dbl;}__内联__float __attribute_((常量))倒数(float x){活接头{浮动单;#ifdef __cplusplus标准::uint_least32_t uint;#其他uint_least32_t单位;#结尾}u;美国单数=x;u.uint=(0xbe6eb3beU-u.uint)>>1;//功率(x,-0.5)u.single*=美国单身;//功率(pow(x,-0.5),2)=功率(x,-1)=1.0/x返回美国单身;}

嗯……我想如果CPU制造商知道在他们设计CPU时,只需一次乘法、减法和位移位就可以近似求倒数。。。。隐马尔可夫模型。。。。。。。。。

至于基准标记,硬件x2结合硬件减法指令的指令与现代计算机上的硬件1.0/x指令一样快(我的基准测试是在Intel i7上进行的,但我会假设其他处理器也有类似的结果)。然而,如果将此算法作为一条新的汇编指令实现到硬件中,那么速度的提高可能足以使此指令变得非常实用。

有关此方法的更多信息,此实现基于“快速”逆平方根算法.

正如Pharap引起我注意的那样,从联合中读取非活动属性是未定义的行为,因此我从他的有益评论中设计了两种可能的解决方案,以避免未定义行为。第一种解决方案看起来更像是绕过一种实际上并不比原始解决方案更好的语言语义的一种令人讨厌的技巧。

#ifdef __cplusplus#包括<cstdint>#其他#包括<stdint.h>#结尾__内联_双__attribute_((常数))倒数(双x){活接头{双dbl[2];#ifdef __cplusplus标准::uint_least64_tull[2];#其他uint_least64_tull[2];#结尾}u;u.dbl[0]=x;//dbl现在是活动属性,因此现在只能读取dblu.ull[1]=0//将ull设置为活动属性以便读取ull的技巧u.ull][0]=(0xbfcdd6a18f6a6f52ULL-u.ull[0])>>1;u.dbl[1]=0;//现在将dbl设置为活动属性,以便可以读取它u.dbl[0]*=u.dbl[0];返回u.dbl[0];}__内联__float __attribute_((常量))倒数(float x){活接头{浮子flt[2];#ifdef __cplusplus标准::uint_least32_tull[2];#其他uint_least32_tull[2];#结尾}u;u.flt[0]=x;//现在flt处于活动状态u.uint[1]=0;//将uint设置为活动的读写u.uint[0]=(0xbe6eb3beU-u.uint[0])>>1;u.flt[1]=0;//让外语教学活跃于阅读和写作u.flt[0]*=u.flt[0];返回u.flt[0];}

第二种可能的解决方案更受欢迎,因为它完全摆脱了工会。然而,如果编译器没有对该解决方案进行适当优化,那么该解决方案将慢得多。但是,从好的方面来看,下面的解决方案将完全不依赖于所提供的字节顺序:

  1. 字节的宽度是8位
  2. 这些字节是目标机器上最小的原子单位。
  3. 双精度数是8字节宽,浮点数是4字节宽。

#ifdef __cplusplus#包括<cstdint>#包括<cstring>#定义stdIntWithEightBits标准::uint8_t#定义stdIntSizeOfFloat标准::uint32_t#定义标准IntSizeOfDouble标准::uint64_t#其他#包括<stdint.h>#包括<string.h>#定义stdIntWithEightBits uint8_t#定义stdIntSizeOfFloat uint32_t#定义双uint64_t的stdIntSizeOf#结尾

__内联_双__attribute_((常数))倒数(双x){双字节IndexFloat=1.1212798184631136e-308//00 08 10 18 20 28 30 38位stdIntWithEightBits*byteIndexs=reinterpret_cast(字节索引浮点);stdIntWithEightBits*inputBytes=reinterpret_cast(&x);stdIntSizeOfDouble输入AsUll=((输入字节[0]<字节索引[0])|(输入字节[1]<<字节索引[1])|(输入字节[2]<<字节索引[2])|(inputBytes[3]<<字节索引[3])|(输入字节[4]<字节索引[4])|(输入字节[5]<字节索引[5])|(输入字节[6]<字节索引[6])|(输入字节[7]<字节索引[7]));inputAsUll=(0xbfcdd6a18f6a6f52ULL-输入AsUll)>>1;双输出双;const stdIntWithEightBits输出字节[]={inputAsUll>>字节索引[0],inputAsUll>>字节索引[1],inputAsUll>>字节索引[2],inputAsUll>>字节索引[3],inputAsUll>>字节索引[4],inputAsUll>>字节索引[5],inputAsUll>>字节索引[6],inputAsUll>>字节索引[7]};memcpy(&outputDouble,&output1Bytes,8);return outputDouble*outputDuuble;}

__内联__float __attribute_((常量))倒数(float x){浮点字节IndexFloat=7.40457e-40;//0x00 08 10 18位stdIntWithEightBits*byteIndexs=reinterpret_cast(字节索引浮点);stdIntWithEightBits*inputBytes=reinterpret_cast(&x);stdIntSizeOfFloat输入AsInt=((输入字节[0]<字节索引[0])|(输入字节[1]<<字节索引[1])|(输入字节[2]<<字节索引[2])|(inputBytes[3]<<字节索引[3]));inputAsInt=(0xbe6eb3beU-inputAsInt)>>1;浮点输出浮点;const stdIntWithEightBits输出字节[]={inputAsInt>>字节索引[0],inputAsInt>>字节索引[1],inputAsInt>>字节索引[2],inputAsInt>>字节索引[3]};memcpy(&outputFloat,&output1Bytes,4);返回outputFloat*outputFloat;}

免责声明:最后,请注意,我在C++方面是一个新手。因此,我热烈欢迎任何最佳实践、适当的格式或含意清晰的编辑,以提高所有阅读者的答案质量,并在今后的岁月里扩展我的C++知识。

16
  • 1
    你可以解释这个神奇的数字,以及它假设的浮点表示。 评论 2016年9月27日2:49
  • 1
    这很有趣。谢谢您!你有精度和速度的对比测试结果吗?
    – 123公里
    评论 2016年9月27日5:11
  • 4
    您是否根据x86的近似倒数指令对此进行了测试,RCPSS系统在你的i7上?它与整数乘法一样快,不需要将数据从XMM寄存器移动到整数。您可以在C++中使用它_mm_rcp_ss(_mm_set_ss(x)).gcc和clang将转换1.0/倍如果您使用-ffast-math,则将其转换为RCPSS+Newton-Raphson迭代,但我认为如果您需要不带近似步骤的值,则必须手动使用内部函数。 评论 2016年11月8日14:40
  • 1
    使用联盟就像这样未定义行为. 评论 2019年6月9日21:30
  • 1
    @Pharap你能推断一下UB的情况吗。我对C++相当缺乏经验,并且在摆脱UB方面做了很大的努力。我想通过解释和/或在线资源了解更多信息。非常感谢您提请我注意这个问题。 评论 2019年6月10日3:26
5

首先,确保这不是一个过早优化的情况。你知道这是你的瓶颈吗?

正如Mystic所说,1/x可以很快计算出来。确保您没有使用双重的1或除数的数据类型。浮动要快得多。

也就是说,基准、基准、基准。不要把时间浪费在数值理论上,只为了发现性能不佳的根源是IO访问。

7
  • 5
    “浮动要快得多”——真的吗?发表这样笼统的声明是危险的。您可以做很多事情来更改编译器生成的代码。它还取决于编译器的目标硬件。例如,在IA32上,gcc在不使用SSE时生成的代码(我认为-mfpmath=387选项)将与double和float的速度相同,因为FPU只处理80位值,任何速度差异都将取决于内存带宽。 评论 2012年3月30日8:41
  • 1
    是的,显然这是一个笼统的声明。但这个问题同样通用。让OP给出细节,我就能做出更“雄辩”的回应。 评论 2012年3月30日8:42
  • 2
    1/x可以快速计算。。但如何使编译器实际发出RCPSS呢? 评论 2012年3月30日9:06
  • 2
    投了反对票,因为大多数答案实际上都没有回答任何问题,还有一小部分甚至没有尝试过。 评论 2017年10月20日13:30
  • 2
    似乎这不是主题-OP要求的是计算浮点快速倒数的方法,而不是优化教程 评论 2018年8月12日1:31

首先,如果启用编译器优化,编译器可能会尽可能优化计算(例如,将其从循环中拉出)。要查看此优化,您需要在发布模式下进行构建和运行。

除法可能比乘法重(但一位评论员指出,在现代CPU上,倒数与乘法一样快,在这种情况下,这不适合您的情况),因此如果您确实有1/X(1/X)出现在循环中的某个地方(并且不止一次),您可以通过在循环中缓存结果来提供帮助(浮子Y=1.0f/X;)然后使用Y(Y)。(编译器优化在任何情况下都可能做到这一点。)

此外,可以重新设计某些公式,以消除除法或其他低效计算。为此,您可以发布正在执行的较大计算。即使在那里,程序或算法本身有时也可以重新构造,以避免频繁地命中耗时的循环。

可以牺牲多少准确性?如果不太可能,你只需要一个数量级,那么你可以很容易地使用模操作符或按位运算得到它。

然而,总的来说,没有办法加快分裂。如果有,编译器早就在这么做了。

  • 如果不太可能,你只需要一个数量级,那么你可以很容易地使用模操作符或按位运算得到它。怎么用?
    – 123公里
    评论 2012年3月30日8:39
  • 1
    我无意暗示“琐碎”。此外,我还应该添加一个警告,即X>>1(见注释末尾)。在这种情况下,您可以利用X^-1=2^(-log_2(X)),并使用en.wikipedia.org/wiki/Find_first_set#算法得到log_2(X)的数量级,得到形式为2^-n的数量级。如果X的上下限已知,这可以用于提高速度。如果计算中的其他量(未在问题中显示)具有已知边界,并且在数量级上有一定的相称性,则可以对其进行缩放并转换为整数。 评论 2012年3月30日9:20
  • 1
    如果您使用-ffast-数学,因此,如果您不打算启用-ffast-数学告诉编译器,您不关心舍入发生在何处/何时/如何进行,或者舍入的顺序。 评论 2016年11月8日15:00

我在Arduino NANO上测试了这些方法的速度和“准确性”。
基本计算是设置变量,Y=15000000和Z=65535
(在我的实际例子中,Y是一个常数,Z可以在65353和3000之间变化,所以这是一个有用的测试)
Arduino上的计算时间是通过将引脚放低,然后按计算值调高,然后再调低,并与逻辑分析仪进行时间比较来确定的。100次循环。变量为无符号整数:-

Y*Z需要0.231毫秒Y/Z需要3.867毫秒。变量为浮动:-Y*Z需要1.066毫秒Y/Z需要4.113毫秒。基本基准点和(1500万/65535=228.885,通过计算器)

使用{Jack G’s}浮点倒数算法:

Y*倒数(Z)需要1.937msec,这是一个很好的改进,但精度要低213.68。

使用{nimig18的}浮点inv_fast:

Y*inv_fast(Z)采用5.501毫秒的精度228.116,单次迭代Y*inv_fast(Z)在第二次迭代中需要7.895毫秒的精度228.883

使用维基百科的Q_rsqrt(由{Jack G}指出)

Y*Q*rsqrt(Z)采用6.104毫秒的精度228.116,单次迭代一切都很有趣,但最终令人失望!
1

这应该通过将许多预先展开的牛顿迭代计算为霍纳多项式来实现,霍纳多项式使用融合乘法累加操作,大多数现代CPU在单个Clk循环中执行(每次):

浮点inv_fast(浮点x){并集{float f;int i;}v;浮点数w,sx;整数m;sx=(x<0)-1:1;x=sx*x;v.i=(int)(0x7EF127EA-*(uint32_t*)&x);w=x*v.f;//霍纳多项式形式的高效迭代逼近改进。v.f=v.f*(2-w);//单次迭代,Err=-3.36e-3*2^(-flr(log2(x)))//v.f=v.f*(4+w*(-6+w*));//第二次迭代,Err=-1.13e-5*2^(-flr(log2(x)))//v.f=v.f*(8+w*(-28+wx(56+w*)(-70+w*第三次迭代,Err=+6.8e-8*2^(-flr(log2(x)))返回v.f*sx;}

精细打印(Fine Print):接近0时,近似值不太好,因此程序员要么需要测试性能,要么在诉诸硬件划分之前限制输入值过低。即负责!

1
  • 你能解释一下位模式0x7EF127EA的含义吗?我想应该将x规范化为~1.0,但如何实现呢?
    – 明2910
    评论 2018年12月17日7:51
0

据我所知,最快的方法是使用SIMD操作。http://msdn.microsoft.com/en-us/library/796k1tty(v=vs.90).aspx

2
  • 1
    或者购买更快的cpu?:)这个问题是算法问题。
    – 123公里
    评论 2014年1月16日12:16
  • RCPSS系统/RCPPS系统是一个很好的建议。x86上的硬件可以提供快速近似求逆(和逆sqrt),用于标量或SIMD浮点向量。您无需在循环的其余部分使用SIMD即可充分利用。如果这个答案解释了其中的任何一点,它就不会得到如此混乱的评论。 评论 2016年11月8日15:03
0

这个rcpss系统汇编指令计算一个近似倒数,|相对误差|≤1.5*2^−12。

可以在编译器上使用-mrecip标志(您可能还需要-ffast-数学).

本质上是_毫米_秒.

1

你的答案

单击“发布您的答案”,表示您同意我们的服务条款并确认您已阅读我们的隐私政策.

不是你想要的答案吗?浏览标记的其他问题问你自己的问题.