正弦值性能的计算与查找表?

假设您必须计算域在0.01到360.01之间的正弦(余弦或正切 – 无论如何)。 (使用C#)

什么会更高效?

  1. 使用Math.Sin
  2. 使用具有预先计算值的查找数组

我会反驳说,鉴于域名,选项2会快得多。 在域精度(0.0000n)的什么时刻,计算的性能超过了查找。

更新:直读到最后。 看起来查找表比Math.Sin更快。

我猜测查找方法会比Math.Sin更快。 我还会说它会快得多,但罗伯特的回答让我觉得我仍然希望对此进行基准测试以确定。 我做了很多音频缓冲处理,我注意到这样的方法:

for (int i = 0; i < audiodata.Length; i++) { audiodata[i] *= 0.5; } 

将执行速度明显快于

 for (int i = 0; i < audiodata.Length; i++) { audiodata[i] = Math.Sin(audiodata[i]); } 

如果Math.Sin和简单乘法之间的差异很大,我猜想Math.Sin和查找之间的差异也很大。

不过,我不知道,我的Visual Studio电脑在地下室,我太累了,不能花2分钟来确定这个。

更新 :好的,测试它花了超过2分钟(更像是20),但看起来Math.Sin的速度至少是查找表的两倍 (使用Dictionary)。 这是使用Math.Sin或查找表执行Sin的类:

 public class SinBuddy { private Dictionary _cachedSins = new Dictionary(); 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); } } 

这是测试/计时代码:

 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.SinLookup只进行直接查找。

这是一台廉价的笔记本电脑(没有双核或任何花哨的东西)。 罗伯特,你这个男人! (但我仍然认为我应该得到支票,因为我做了工作)。

更新2 :好的,我彻底迟钝了。 事实certificate停止并重新启动秒表不会重置经过的毫秒数,因此查找只有一半的速度,因为它的时间包括Math.Sin调用的时间。 此外,我重读了这个问题并意识到你正在谈论在一个简单的数组中缓存值,而不是使用Dictionary。 这是我修改后的代码(我将旧代码留作后代警告):

 public class SinBuddy { private Dictionary _cachedSins = new Dictionary(); 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); } } 

测试/定时代码:

 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毫秒,而基于arrays的查找表大约需要600毫秒。 所以看起来(正确编写的[gulp])查找表实际上比使用Math.Sin快一点 ,但不是很多。

请自行validation这些结果,因为我已经certificate了我的无能。

过去,数组查找是执行快速触发计算的一个很好的优化。

但是,对于缓存命中,内置数学协处理器(使用表查找)和其他性能改进,最好自己计算特定代码以确定哪些代码会更好。

对于性能问题,唯一正确的答案是您在测试后达到的答案。 但是,在测试之前,您需要确定测试的工作是否值得花时间 – 这意味着您已经确定了性能问题。

如果您只是好奇,可以轻松编写测试来比较速度。 但是,您需要记住,使用查找表的内存可能会影响较大应用程序中的分页。 因此,即使在小测试中分页速度更快,也可能会在使用更多内存的较大应用中降低速度。

由于您将傅里叶变换作为应用程序提及,您可能还会考虑使用方程计算正弦/余弦

sin(x + y)= sin(x)cos(y)+ cos(x)sin(y)

cos(x + y)= cos(x)cos(y) – sin(x)sin(y)

即你可以从sin((n-1)* x),cos((n-1)* x迭代计算n = 0,1,2 …的sin(n * x),cos(n * x) )和常数sin(x),cos(x)有4次乘法。 当然,只有在必须对算术序列求值sin(x),cos(x)时才有效。

在没有实际实施的情况下比较这些方法是困难的。 这很大程度上取决于您的表格适合缓存的程度。

答案完全取决于查找表中有多少个值。 您说“域名介于0.01和360.01之间”,但您没有说明可能会使用该范围内的多少值,或者您需要答案的准确程度。 请原谅我不希望看到用于在非科学背景下传达隐含意义的有效数字。

仍然需要更多信息来回答这个问题。 0.01到360.01之间的预期值分布是多少? 除了简单的sin()计算之外,您是否正在处理大量数据?

36000双精度值在内存中占用超过256k; 查找表太大,无法容纳大多数机器上的L1缓存; 如果你直接穿过桌子,你会在每个sizeof(cacheline)/ sizeof(double)访问时错过L1,并且可能会命中L2。 另一方面,如果您的表访问或多或少是随机的,那么几乎每次进行查找时都会丢失L1。

它也很大程度上取决于您所使用的平台的数学库。 例如,sin函数的常见i386实现范围从大约40个周期到400个周期甚至更多,具体取决于您的确切微体系结构和库供应商。 我还没有计时微软库,所以我不知道C#Math.sin实现的确切位置。

由于来自L2的负载通常比理智平台上的40个周期快,因此可以合理地期望查找表可以更快地被隔离考虑。 但是,我怀疑你是孤立地计算罪恶(); 如果sin()的参数跳到表中,那么你将从缓存中将其他计算步骤所需的其他数据吹掉; 虽然sin()计算变得更快,但计算其他部分的减速可能超过加速。 只有仔细的测量才能真正回答这个问题。

我是否可以从您的其他评论中了解到,您正在将此作为FFT计算的一部分? 您是否有理由需要使用自己的FFT而不是使用已存在的众多极高质量实现中的一种?

Math.Sin更快。 写作的人很聪明,并且在准确和快速时使用表格查找,并在更快的时候使用数学。 并且没有什么关于该领域使其特别快,大多数trig函数实现的第一件事是无论如何映射到有利的域。

由于您的查找表中可能有数千个值,您可能想要做的是使用字典,当您计算值时,将其放入字典中,这样您只需计算一次每个值,并使用C#函数做计算。

但是,没有理由一遍又一遍地重新计算相同的值。