Calculating vs. lookup tables for sine value performance?
假设你必须计算出正弦波(余弦或正切——无论什么),其中的域在0.01和360.01之间。(用C语言)
什么会更有效?
我会预料到,给定域,选项2会更快。在域精度(0.0000n)中的哪个点,计算的性能是否超过查找。
更新:通读到底。看起来查找表比Math.sin快。
我想查找方法会比math.sin更快。我也会说这会快得多,但罗伯特的回答让我觉得我仍然希望以此作为基准来确定。我做了很多音频缓冲处理,我注意到这样的方法:
1 2 3 4 | for (int i = 0; i < audiodata.Length; i++) { audiodata[i] *= 0.5; } |
执行速度明显快于
1 2 3 4 | for (int i = 0; i < audiodata.Length; i++) { audiodata[i] = Math.Sin(audiodata[i]); } |
如果math.sin和简单乘法之间的差异很大,我想math.sin和lookup之间的差异也很大。
不过,我不知道,我的带Visual Studio的电脑在地下室,我太累了,无法用2分钟来确定这个问题。
更新:好的,测试这个花了2分钟多的时间(大约20分钟),但看起来像数学。sin的速度至少是查找表(使用字典)的两倍。下面是使用math.sin或查找表执行sin的类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | public class SinBuddy { private Dictionary<double, double> _cachedSins = new Dictionary<double, double>(); private const double _cacheStep = 0.01; private double _factor = Math.PI / 180.0; public SinBuddy() { for (double angleDegrees = 0; angleDegrees <= 360.0; angleDegrees += _cacheStep) { double angleRadians = angleDegrees * _factor; _cachedSins.Add(angleDegrees, Math.Sin(angleRadians)); } } public double CacheStep { get { return _cacheStep; } } public double SinLookup(double angleDegrees) { double value; if (_cachedSins.TryGetValue(angleDegrees, out value)) { return value; } else { throw new ArgumentException( String.Format("No cached Sin value for {0} degrees", angleDegrees)); } } public double Sin(double angleDegrees) { double angleRadians = angleDegrees * _factor; return Math.Sin(angleRadians); } } |
下面是测试/计时代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | SinBuddy buddy = new SinBuddy(); System.Diagnostics.Stopwatch timer = new System.Diagnostics.Stopwatch(); int loops = 200; // Math.Sin timer.Start(); for (int i = 0; i < loops; i++) { for (double angleDegrees = 0; angleDegrees <= 360.0; angleDegrees += buddy.CacheStep) { double d = buddy.Sin(angleDegrees); } } timer.Stop(); MessageBox.Show(timer.ElapsedMilliseconds.ToString()); // lookup timer.Start(); for (int i = 0; i < loops; i++) { for (double angleDegrees = 0; angleDegrees <= 360.0; angleDegrees += buddy.CacheStep) { double d = buddy.SinLookup(angleDegrees); } } timer.Stop(); MessageBox.Show(timer.ElapsedMilliseconds.ToString()); |
使用0.01度的阶跃值并在整个值范围内循环200次(如本代码所示),使用math.sin大约需要1.4秒,使用字典查找表大约需要3.2秒。将步长值降低到0.001或0.0001会使查找在math.sin中执行得更差。而且,这个结果更倾向于使用math.sin,因为sinbuddy.sin做了一个乘法,在每次调用中都将角度以度数转换为弧度角度,而sinbuddy.sin lookup只做一个直接的查找。
这是一个便宜的笔记本电脑(没有双核或任何花哨的东西)。罗伯特,你这个混蛋!(但我仍然认为我应该拿到支票,因为我做了这项工作)。
更新2:好吧,我是个白痴…结果是停止和重新启动秒表并没有重置经过的毫秒数,所以查找的速度只有原来的一半,因为时间包括了Math.sin调用的时间。另外,我重读了这个问题,意识到您所说的是将值缓存在一个简单的数组中,而不是使用字典。这是我修改的代码(我将保留旧代码作为对后代的警告):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | public class SinBuddy { private Dictionary<double, double> _cachedSins = new Dictionary<double, double>(); private const double _cacheStep = 0.01; private double _factor = Math.PI / 180.0; private double[] _arrayedSins; public SinBuddy() { // set up dictionary for (double angleDegrees = 0; angleDegrees <= 360.0; angleDegrees += _cacheStep) { double angleRadians = angleDegrees * _factor; _cachedSins.Add(angleDegrees, Math.Sin(angleRadians)); } // set up array int elements = (int)(360.0 / _cacheStep) + 1; _arrayedSins = new double[elements]; int i = 0; for (double angleDegrees = 0; angleDegrees <= 360.0; angleDegrees += _cacheStep) { double angleRadians = angleDegrees * _factor; //_cachedSins.Add(angleDegrees, Math.Sin(angleRadians)); _arrayedSins[i] = Math.Sin(angleRadians); i++; } } public double CacheStep { get { return _cacheStep; } } public double SinArrayed(double angleDegrees) { int index = (int)(angleDegrees / _cacheStep); return _arrayedSins[index]; } public double SinLookup(double angleDegrees) { double value; if (_cachedSins.TryGetValue(angleDegrees, out value)) { return value; } else { throw new ArgumentException( String.Format("No cached Sin value for {0} degrees", angleDegrees)); } } public double Sin(double angleDegrees) { double angleRadians = angleDegrees * _factor; return Math.Sin(angleRadians); } } |
测试/计时代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | SinBuddy buddy = new SinBuddy(); System.Diagnostics.Stopwatch timer = new System.Diagnostics.Stopwatch(); int loops = 200; // Math.Sin timer.Start(); for (int i = 0; i < loops; i++) { for (double angleDegrees = 0; angleDegrees <= 360.0; angleDegrees += buddy.CacheStep) { double d = buddy.Sin(angleDegrees); } } timer.Stop(); MessageBox.Show(timer.ElapsedMilliseconds.ToString()); // lookup timer = new System.Diagnostics.Stopwatch(); timer.Start(); for (int i = 0; i < loops; i++) { for (double angleDegrees = 0; angleDegrees <= 360.0; angleDegrees += buddy.CacheStep) { double d = buddy.SinLookup(angleDegrees); } } timer.Stop(); MessageBox.Show(timer.ElapsedMilliseconds.ToString()); // arrayed timer = new System.Diagnostics.Stopwatch(); timer.Start(); for (int i = 0; i < loops; i++) { for (double angleDegrees = 0; angleDegrees <= 360.0; angleDegrees += buddy.CacheStep) { double d = buddy.SinArrayed(angleDegrees); } } timer.Stop(); MessageBox.Show(timer.ElapsedMilliseconds.ToString()); |
这些结果完全不同。使用math.sin大约需要850毫秒,字典查找表大约需要1300毫秒,基于数组的查找表大约需要600毫秒。因此,一个(正确编写的[gulp])查找表实际上比使用math.sin快一点,但不是很多。
请你自己核实这些结果,因为我已经证明了我的无能。
过去,数组查找是执行快速Trig计算的一个很好的优化。
但是,对于缓存命中率、内置数学协处理器(使用表查找)和其他性能改进,最好是自己确定特定代码的时间,以确定哪一个性能更好。
对于性能问题,唯一正确的答案是测试后得到的答案。但是,在测试之前,您需要确定测试的努力是否值得您花时间——这意味着您已经确定了一个性能问题。
如果你只是好奇,你可以很容易地写一个测试来比较速度。但是,您需要记住,使用内存查找表可能会影响大型应用程序中的分页。因此,即使在小测试中分页速度更快,在使用更多内存的大型应用程序中,分页速度也会减慢。
既然你提到傅立叶变换作为一个应用,你也可以考虑用这些方程来计算你的正弦/余弦。
sin(x+y) = sin(x)cos(y) + cos(x)sin(y)
cos(x+y) = cos(x)cos(y) - sin(x)sin(y)
也就是说,你可以计算sin(n*x),cos(n*x),n=0,1,2…从sin((n-1)*x)、cos((n-1)*x)和常量sin(x)、cos(x)迭代4次乘法。当然,只有当你必须在一个算术序列上计算sin(x),cos(x)的时候,这才有效。
比较没有实际实施的方法是困难的。这很大程度上取决于表在缓存中的容纳程度。
答案完全取决于查找表中有多少值。您会说"域在0.01和360.01之间",但您不会说该范围内可能使用了多少值,或者您需要的答案有多精确。原谅我,因为不希望看到用于在非科学上下文中传达隐含含义的有效数字。
回答这个问题还需要更多的信息。0.01和360.01之间的预期值分布是什么?除了简单的sin()计算,您是否处理了大量数据?
36000个双精度值占用了256K以上的内存;查找表太大,在大多数计算机上无法容纳在一级缓存中;如果直接在表中运行,则在每个sizeof(cacheline)/sizeof(double)访问中可能会错过一次l1,并且可能会碰到l2。另一方面,如果您的表访问或多或少是随机的,那么几乎每次进行查找时都会丢失l1。
它还很大程度上取决于您所处平台的数学库。例如,sin函数的常见i386实现范围从约40个周期到400个周期甚至更多,这取决于您的精确微体系结构和库供应商。我还没有对Microsoft库进行计时,因此我不知道C Math.sin实现将落在何处。
由于在一个健全的平台上,来自l2的负载通常比40个周期要快,因此我们可以合理地期望在孤立的情况下更快地考虑查找表。但是,我怀疑您是在孤立地计算sin();如果您对sin()的参数跳到表中,您将把其他计算步骤所需的其他数据从缓存中吹走;尽管sin()计算速度更快,但对其他计算部分的减速可能会超过加速。只有仔细的测量才能真正回答这个问题。
从你的其他评论中,我能理解你是在做FFT计算的一部分吗?是否有理由需要滚动自己的fft,而不是使用已经存在的众多非常高质量的实现之一?
数学。罪恶更快。写东西的人很聪明,在准确和快速的时候使用表格查找,在快速的时候使用数学。而且这个域没有什么特别快的地方,大多数trig函数实现所做的第一件事就是映射到一个有利的域。
因为查阅表格中可能有数千个值,所以您可能需要使用字典,当您计算值时,请将其放入字典中,这样您只需计算一次每个值,并使用C函数进行计算。
但是,没有理由反复重新计算相同的值。