使用预先计算的平移阵列的快速Sin / Cos

时间:2010-01-18 18:17:48

标签: c# performance optimization

我有以下代码使用预先计算的内存表执行Sin / Cos函数。在下面的示例中,该表具有1024 * 128项,涵盖从0到2pi的所有Sin / Cos值。我知道我可以使用Sin / Cos对称并只保留1/4的值但是在计算值时我会有更多'ifs'。

private const double PI2 = Math.PI * 2.0; 
private const int TABLE_SIZE = 1024 * 128;
private const double TABLE_SIZE_D = (double)TABLE_SIZE;
private const double FACTOR = TABLE_SIZE_D / PI2;

private static double[] _CosineDoubleTable;
private static double[] _SineDoubleTable;

设置转换表

private static void InitializeTrigonometricTables(){
   _CosineDoubleTable = new double[TABLE_SIZE];
   _SineDoubleTable = new double[TABLE_SIZE];

   for (int i = 0; i < TABLE_SIZE; i++){
      double Angle = ((double)i / TABLE_SIZE_D) * PI2;
      _SineDoubleTable[i] = Math.Sin(Angle);
      _CosineDoubleTable[i] = Math.Cos(Angle);
   }
}

价值是弧度的两倍。

Value %= PI2;  // In case that the angle is larger than 2pi
if (Value < 0) Value += PI2; // in case that the angle is negative
int index = (int)(Value * FACTOR); //from radians to index and casted in to an int
double sineValue = _SineDoubleTable[index]; // get the value from the table

我正在寻找更快的方法来做到这一点。以上4行是整个过程的约25%(执行数十亿次)。

7 个答案:

答案 0 :(得分:21)

您可以尝试使用不安全的代码来消除数组边界检查 但即使是不安全的优化版本似乎也不会出现在Math.Sin附近。

基于具有随机值的1'000'000'000次迭代的结果:

(1) 00:00:57.3382769  // original version
(2) 00:00:31.9445928  // optimized version
(3) 00:00:21.3566399  // Math.Sin

代码:

static double SinOriginal(double Value)
{
    Value %= PI2;
    if (Value < 0) Value += PI2;
    int index = (int)(Value * FACTOR);
    return _SineDoubleTable[index];
}

static unsafe double SinOptimized(double* SineDoubleTable, double Value)
{
    int index = (int)(Value * FACTOR) % TABLE_SIZE;
    return (index < 0) ? SineDoubleTable[index + TABLE_SIZE]
                       : SineDoubleTable[index];
}

测试程序:

InitializeTrigonometricTables();
Random random = new Random();

SinOriginal(random.NextDouble());
var sw = System.Diagnostics.Stopwatch.StartNew();
for (long i = 0; i < 1000000000L; i++)
{
    SinOriginal(random.NextDouble());
}
sw.Stop();
Console.WriteLine("(1) {0}  // original version", sw.Elapsed);

fixed (double* SineDoubleTable = _SineDoubleTable)
{
    SinOptimized(SineDoubleTable, random.NextDouble());
    sw = System.Diagnostics.Stopwatch.StartNew();
    for (long i = 0; i < 1000000000L; i++)
    {
        SinOptimized(SineDoubleTable, random.NextDouble());
    }
    sw.Stop();
    Console.WriteLine("(2) {0}  // optimized version", sw.Elapsed);
}

Math.Sin(random.NextDouble());
sw = System.Diagnostics.Stopwatch.StartNew();
for (long i = 0; i < 1000000000L; i++)
{
    Math.Sin(random.NextDouble());
}
sw.Stop();
Console.WriteLine("(3) {0}  // Math.Sin", sw.Elapsed);

答案 1 :(得分:8)

我假设泰勒的扩张对你没用。所以如果你想使用一个表: 你只需要一张大一半的桌子。

  1. cos(x) = sin(pi/2-x).
  2. sin(pi + x) = -sin(x)
  3. 您可以将您的代码设为非分支。 首先转换为int格式。

    int index = (int)(Value * FACTOR);
    index %= TABLE_SIZE; // one instuction (mask)
    index = (index >= 0) ? index :TABLE_SIZE-index; // one instruction isel
    double sineValue = _SineDoubleTable[index];
    

    无论如何要与Math.Sin比较。简介简介Priofile。 (在实际示例中,缓存未命中可能会降低代码速度。)

答案 2 :(得分:4)

如果你需要多次计算,

  1. 使用特定于处理器的数学库,例如IKMLACML
    1. 计算组(向量)中的值。
    2. 当你需要两者时,总是同时计算一个值的sin和cos。
  2. 检查算法的复杂性和实现设计。
  3. 确保您正在使用所有处理器提供的功能 - x64架构,以及任何有用的矢量说明。

答案 3 :(得分:2)

除了mod操作外,这看起来还不错。你能没有吗?

如果值接近零,则可以使用

while(Value > PI2) Value -= PI2;
while(Value < 0) Value += PI2;

或者首先将索引转换为整数(可能超出范围)可能会更快,然后将其修改为整数。如果表大小将是2的倍数,您甚至可以使用位操作(如果编译器不已执行此操作)。

答案 4 :(得分:2)

不能保证它会带来很多好处,但是根据你的处理器,整数数学通常比浮点数学更快。在这种情况下,我可能会重新排列前三行,先计算一个整数,然后减小其范围(如果需要)。当然,正如BlueRaja指出的那样,使用C ++几乎肯定也会有所帮助。使用汇编语言可能不会有太大的好处 - 对于像这样的表查找,C ++编译器通常可以生成相当好的代码。

如果可能的话,我也会非常努力地看待你的准确性要求 - 不知道你对这些价值做了什么,很难说,但是对于很多的目的,你的表格大小和您存储的精度超出了必要或甚至接近有用的范围。

最后,我注意到至少值得研究整个策略是否值得。毫无疑问,使用表来避免复杂计算是一个可靠的策略。尽管如此,处理器加快了 lot 的速度 - 到目前为止,这样的表查找通常是净损失。事实上,几乎唯一的方法是表格是否足够小,以适应处理器缓存。

答案 5 :(得分:0)

这将是非常快的。

如果你真的需要从这段代码中挤出所有可以想象的性能下降,你可能要考虑在C ++ dll(甚至是ASM)中编写它的这一部分(包括循环数十亿次的循环)。确保您的编译器设置为允许您使用最大可能的指令集。

[编辑] 我错过了表的大小 - 由于缓存未命中,这可能会大大减慢您的代码速度。您是否尝试过针对Math.Cos()或其他近似trig函数的方法进行基准测试(使用Taylor Series可以通过一些简单的乘法获得非常好的近似值)

答案 6 :(得分:0)

你可以尝试的一件事是使用cos(x)= sin(x + pi / 2)这一事实。并使正弦表大四分之一,因此您可以将其重新用作从四分之一开始的余弦表。不确定C#是否允许您获得指向表格中间的指针,如C所示。但即使没有,减少的缓存使用量可能比增加到正弦表的偏移量的时间更多。

那,用C表示:

double* _CosineDoubleTable = &_SineDoubleTable[TABLESIZE / 4];