c++三角函数的快速实现
Posted
技术标签:
【中文标题】c++三角函数的快速实现【英文标题】:Fast implementation of trigonometric functions for c++ 【发布时间】:2011-04-25 09:43:31 【问题描述】:简短版:我想知道标准三角函数的实现是否比math.h
中包含的函数更快。
长版:我有一个程序非常依赖数字(它是一个物理模拟),需要调用三角函数,主要是 sin
和 cos
,很多。目前我只是使用math.h
中包含的实现。分析表明,对这些函数的调用成本超出了我的预期(希望)。
虽然在代码的其他部分肯定有足够的优化空间,但更快的sin
和cos
可能会给我一些额外的百分比。那么,你们有什么建议吗?
在另一个post 中建议使用自制查找表。但也许还有其他选择?还是某些库中现成且经过良好测试的查找解决方案?
【问题讨论】:
大多数快速超越的都是面向游戏引擎的,它们不太关心准确性。准确性对您的问题有多重要? 个人资料第一。 “可能会提供一些额外的百分比”不值得尝试优化。 @pmr:正如我的问题中所述,我正在分析,因此我的预期是运行时的“百分之几”——可能是 2% 或 3%,但这肯定是一个非常粗略的估计.但由于运行时间只有几天,我能得到的任何百分比都可能确实值得.. 查找表有点像 1985 年。现代 CPU 在处理数字方面比从内存中读取要快得多。除非您的查找表非常小,并且您批量执行很多 sin/cos,否则您可以保证 LUT 位于 1 级缓存中,否则不值得。我已经看到 SSE 中的 minimax polys 可以在 18-20 个周期内有效运行(流水线 ftw)。这大约是 LUT 最佳情况的两倍,并且比平均情况稍快,尤其是如果您执行的不是合成基准测试(但是,它不会从其他代码中删除缓存行)。 我在 cpu 端实现了一个快速正弦函数,它至少比 math.h 的正弦函数快 两倍,但是我使用了一个非常小的查找表( 20 个浮子)。它的准确性也不错; 平均相对错误率为 0.095%。你可以从http://www.hevi.info/tag/fast-sine-function/查看它 【参考方案1】:这里有一些很好的幻灯片,介绍了如何对三角函数进行幂级数逼近(虽然不是泰勒级数):Faster Math Functions。
它面向游戏程序员,这意味着牺牲精度来换取性能,但您应该能够在近似值上再添加一两个术语以恢复一些精度。
这样做的好处是您还应该能够轻松地将其扩展到 SIMD,这样您就可以同时计算 4 个值的 sin 或 cos(如果您使用双精度,则为 2)。
希望对您有所帮助...
【讨论】:
您的链接中提供的演示文稿似乎很有趣。我将进一步研究这些近似值,也许这对于我的代码的某些部分确实足够了 我将此标记为已接受的答案,因为在上述 URL 链接的演示文稿中有很多有趣的建议。但不要错过其他答案.. 链接已失效,这是存档中最后看到的版本:web.archive.org/web/20160322120707/http://www.research.scea.com/… 作者的博客文章,其中包含演示文稿和其他 cmets:basesandframes.wordpress.com/2016/05/17/faster-math-functions 您可以使用 AVX/AVX-512 一次操作 4 或 8 个双打【参考方案2】:如果您可以进一步优化它,这应该非常快,请在 paste.org 之类的地方发布代码。
电脑规格 -> 512MB Ram , Visual Studio 2010 , Windows XP Professional SP3 Version 2002 , Intel (R) Pentium (R) 4 CPU 2.8GHZ。
这是非常准确的,并且在某些情况下实际上会提供稍微更好的结果。例如。 C++ 中的 90、180、270 度返回非 0 小数。
0 到 359 度的完整表格:https://pastee.org/dhwbj
格式 -> 度数 # -> MINE_X(#) , CosX(#) , MINE_Z(#) , SinZ(#)。
以下是用于构建上述表格的代码。如果您使用更大的数据类型,您可能会使其更加准确。我使用了一个无符号的短并做了 N/64000。那么最接近我的 cos(##) 和 sin(##) 是什么,四舍五入到该索引。我还尝试使用尽可能少的额外数据,这样就不会是一些杂乱无章的表格,其中 cos 和 sin 有 720 个浮点值。这可能会产生更好的结果,但会完全浪费内存。下表尽可能小。我想看看是否有可能制作一个可以四舍五入到所有这些短值并使用它的方程。我不确定它是否会更快,但它会完全消除桌子,并且可能不会降低速度。
因此,与 C++ cos/sin 运算相比,准确度为 99.99998% 到 100%。
下表是用于计算 cos/sin 值的表格。
static const unsigned __int16 DEGREE_LOOKUP_TABLE[91] =
64000, 63990, 63961, 63912, 63844, 63756,
63649, 63523, 63377, 63212, 63028, 62824,
62601, 62360, 62099, 61819, 61521, 61204,
60868, 60513, 60140, 59749, 59340, 58912,
58467, 58004, 57523, 57024, 56509, 55976,
55426, 54859, 54275, 53675, 53058, 52426,
51777, 51113, 50433, 49737, 49027, 48301,
47561, 46807, 46038, 45255, 44458, 43648,
42824, 41988, 41138, 40277, 39402, 38516,
37618, 36709, 35788, 34857, 33915, 32962,
32000, 31028, 30046, 29055, 28056, 27048,
26031, 25007, 23975, 22936, 21889, 20836,
19777, 18712, 17641, 16564, 15483, 14397,
13306, 12212, 11113, 10012, 8907, 7800,
6690, 5578, 4464, 3350, 2234, 1117,
0,
;
以下是进行 cos/sin 计算的实际代码。
int deg1 = (int)degrees;
int deg2 = 90 - deg1;
float module = degrees - deg1;
double vX = DEGREE_LOOKUP_TABLE[deg1] * 0.000015625;
double vZ = DEGREE_LOOKUP_TABLE[deg2] * 0.000015625;
double mX = DEGREE_LOOKUP_TABLE[deg1 + 1] * 0.000015625;
double mZ = DEGREE_LOOKUP_TABLE[deg2 - 1] * 0.000015625;
float vectorX = vX + (mX - vX) * module;
float vectorZ = vZ + (mZ - vZ) * module;
if (quadrant & 1)
float tmp = vectorX;
if (quadrant == 1)
vectorX = -vectorZ;
vectorZ = tmp;
else
vectorX = vectorZ;
vectorZ = -tmp;
else if (quadrant == 2)
vectorX = -vectorX;
vectorZ = -vectorZ;
速度低于使用最初提到的计算机规格。在这是调试模式之前,我在调试模式下运行它,但是通过我认为在没有调试的情况下调试的可执行文件运行。
我的方法
1,000 Iterations -> 0.004641 MS or 4641 NanoSeconds.
100,000 Iterations -> 4.4328 MS.
100,000,000 Iterations -> 454.079 MS.
1,000,000,000 Iterations -> 4065.19 MS.
COS/SIN 法
1,000 Iterations -> 0.581016 MS or 581016 NanoSeconds.
100,000 Iterations -> 25.0049 MS.
100,000,000 Iterations -> 24,731.6 MS.
1,000,000,000 Iterations -> 246,096 MS.
所以总结一下上面我的策略同时执行 cos(###) 和 sin(###) 允许每秒大约 220,000,000 次执行。使用最初显示的计算机规格。这相当快并且使用的内存非常少,因此它可以很好地替代 C++ 中通常发现的数学 cos/sin 函数。如果您想查看准确性,请打开上面显示的链接,并且打印出 0 度到 359 度。这也支持 0 到 89 和 0 到 3 象限。所以您需要使用它或执行(度 % 90)。
【讨论】:
sin(90)
在 C++ 中不为 0 的原因很简单:C++ 使用弧度,而不是度数。
有道理我从来没有真正想过它,因为它的值是如此微小,它基本上是 0。虽然我猜是除以 180 并乘以 PI。可能很难保证您会得到 90、180 和 270 的弧度值。
结果表的链接无效。最好知道以 ULP 单位表示的最大误差是多少。可能很难准确计算。至少实验结果(但对范围 0 - 360 进行更精细的划分)会有所帮助。
一个没有得到充分强调的问题是您的方法没有减少参数,因此与标准库的性能比较是不公平的。
一个明显的优化是将* 0.000015625
合并到表中。这将消除每次调用的四个浮点乘法。【参考方案3】:
Quake 3 的源代码有一些预先计算的正弦/余弦代码,旨在速度超过精度,它不是基于 sse 的,因此非常便携(在架构和内在 api 上)。您可能还会发现这个基于 sse 和 sse2 的函数的摘要非常有趣:http://gruntthepeon.free.fr/ssemath/
【讨论】:
【参考方案4】:如果您想使用自定义实现,请查看 here、here 和 here
如果您需要计算大型数组的 sin/cos,也可以使用 here(滚动到通用 SIMD 数学库)
您也可以尝试使用 C++ SSE 内在函数。看here
请注意,大多数现代编译器都支持 SSE 和 SSE2 优化。例如,对于 Visual Studio 2010,您需要手动启用它。完成此操作后,大多数标准数学函数将使用不同的实现。
另一种选择是使用 DirectX HLSL。看here。请注意,有一个很好的 sincos 函数可以返回 sin 和 cos。
通常,我使用 IPP(它不是免费的)。详情请看here
【讨论】:
有趣的链接。谢谢!不幸的是,我无法使用 IPP,但我会阅读更多关于其他解决方案的内容。【参考方案5】:我在 cpu 端实现了一个快速正弦函数,它至少比 math.h 的正弦函数快两倍,但是我使用了一个非常小的查找表(20 个浮点数)。它的准确性也不错;平均相对错误率为0.095%。你可以从http://www.hevi.info/tag/fast-sine-function/查看它
该方法的解释非常简单,并且依赖于一个事实,即对于小 a 的 sin(a) = a * pi / 180(证明见上面的链接)
一些三角函数
虽然对于 0 到 10 之间的角度,使用上面显示的公式可以获得相对准确的结果,但角度会随着精度的下降而变宽。因此,我们应该对小于 10 的角度使用公式,但是如何使用呢?!
答案来自三角正弦加法公式;
sin(a+b) = sin(a) cos(b) + sin(b) cos(a)
如果我们可以保持“b”小于 10,那么我们将能够使用我们的公式通过几个算法运算来找到正弦。
假设我们被问到 71.654 的正弦值,那么;
a = 70
b = 1.654
和,
sin(71.654) = sin(70 + 1.654) = sin(70) cos(1.654) + sin(1.654) cos (70)
在这个公式中,我们能够对 sin(1.654) 部分使用快速计算,不幸的是,对于其余部分,我们需要有正弦和余弦表。好消息是我们只需要乘以 10 的正弦和 0 到 10 之间的自然数角度的余弦。
【讨论】:
【参考方案6】:A) 试图节省一小部分不会很令人满意。在 97 小时而不是 100 小时内完成仍然很长。
B) 您说您进行了分析,并且三角函数花费的时间比您希望的要多。 多少?剩下的时间呢? 很有可能你有更大的鱼要炸。 大多数分析器based on the gprof concepts 不会告诉您有关堆栈中调用的信息,您可以专注于这些调用以节省大量时间。 Here's an example.
【讨论】:
毫无疑问,我的代码中有更大的鱼在游动。我正在对结构和算法进行一些更改,希望能带来更显着的改进。但是,当我在寻找大问题时,我在我的清单上列出了一些可能值得研究的小问题。这是其中之一。顺便说一句,我正在使用 callgrind (valgrind) 和 AMD CodeAnalyst @janitor048:很好。这些工具的问题是它们经常将您的注意力集中在小/不相关的东西上。每当我解决性能问题时,我都会依赖this method。它不是一个工具。这是一种技术,而且与任何技术一样有效。 是的,我读过你的那篇文章.. :-) 非常有趣的论证和非常直观的方法。我认为 AMD 的 CodeAnalyst(我使用)的“基于时间的分析”方案基本上是您方法的自动化版本。但我当然只是触及了这个(非常复杂的)领域的表面......【参考方案7】:很久以前,人们在慢速机器上使用具有预先计算值的数组。另一种以您自己的精度计算的选项,例如this:(查找“系列定义”)
【讨论】:
【参考方案8】:您可以查看this。它谈到优化 sin, cos。
【讨论】:
【参考方案9】:对于 2-3% 的收益,这几乎肯定不值得冒不准确、错误、假设不再正确(例如永远不会超出 [-1,-1]
)等风险,除非您计划在大量机器(其中 2-3% 代表数千或数百万美元的电力和机器的摊销成本)。
也就是说,如果您对要完成的工作具有特定领域的知识,则可以将计算速度提高两倍或更多。例如,如果您总是需要相同值的 sin
和 cos
,请在代码中计算它们彼此接近,并确保您的编译器将它们转换为 FSINCOS 汇编指令(参见 this question)。如果您只需要函数全部范围的一小部分,您可以使用一组低阶多项式,然后使用牛顿法的迭代来获得完整的机器精度(或尽可能多)。同样,如果你知道你只需要一些值,这会更强大——例如。如果您可以使用 sin(x) 接近于 x 接近零,并且您只需要接近零的值,那么您可以显着减少您需要的项数。
但是,我的主要建议是:2-3% 不值得。在优化之前,请仔细考虑所使用的算法和其他潜在的瓶颈(例如 malloc 是否占用了太多时间?)。
【讨论】:
不,不会是数百万美元 :-) 但是代码在一些大学计算集群上运行。而且它越快,它获得的插槽就越好。当然你是对的。我不会关注这个问题,还有更严重的瓶颈——这个 sin/cos 业务是我放在“可能值得研究”列表中的一个小问题,我想知道是否有潜力进行改进。这里有一些有趣的建议..以上是关于c++三角函数的快速实现的主要内容,如果未能解决你的问题,请参考以下文章