【问题标题】:Fast Sin/Cos using a pre computed translation array使用预先计算的平移数组的快速正弦/余弦
【发布时间】:2010-01-18 18:17:48
【问题描述】:

我有以下代码使用预先计算的内存表执行正弦/余弦函数。在以下示例中,该表有 1024*128 个项目,涵盖从 0 到 2pi 的所有正弦/余弦值。我知道我可以使用正弦/余弦对称性并仅保留 1/4 的值,但在计算值时我会有更多的“如果”。

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%(执行数十亿次)。

【问题讨论】:

  • 您是否进行过基准测试,看看这种预计算是否真的提高了性能?
  • +1 因为有这么一个可笑的独特问题。
  • 是否有可能将优化点转移到调用三角函数查找的代码上?例如,重新排序输入数据以便您可以利用缓存计算出的 Sin/Cos 值?
  • 缓存未命中通常会导致超过 2 个数量级的延迟,您确定将其保存在内存中不会降低性能吗?
  • 你正在使用所有的核心,不是吗?

标签: c# performance optimization


【解决方案1】:

您可以尝试使用不安全的代码来消除数组边界检查。
但即使是一个不安全的优化版本似乎也比不上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 来自我,即使这不是最快的(尽管我迫不及待地想明天测试一下)
  • +1 来自我 :) - 你能与 math.sin 相比吗?并执行 sin(x) 然后执行 cos (x) 以获得一些缓存 thrash :)
  • +1 过早优化,万恶之源等。这里的好课
  • 我也是+1。查找表从来没有真正在现代处理器上工作——随机访问内存是当今的一个重大瓶颈。改善这一点的唯一真正方法是使用矢量处理 (SIMD / SSE),但这确实取决于所使用的算法。
【解决方案2】:

我假设泰勒展开式对你没用。因此,如果您想使用表格: 你只需要一张一半大的桌子。

  1. cos(x) = sin(pi/2-x).
  2. sin(pi + x) = -sin(x)

您可以使您的代码不分支。 先转换成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 进行比较。简介简介简介。 (在实际示例中,缓存未命中可能会减慢您的代码速度。)

【讨论】:

  • 好东西。对于一些真实世界的代码(具有紧密的内部循环),我发现使用代码查找至少比使用 Math.Sin 快两倍。对于您的 FACTOR 变量,我刚刚使用了 TABLE_SIZE 并且不担心表大小可能减半。也许使用 unsafe 会有所帮助...
【解决方案3】:

如果你要计算这么多次,

  1. 使用特定于处理器的数学库,例如 IKMLACML
    1. 按组(向量)计算值。
    2. 当您需要两者时,请始终同时计算一个值的 sin 和 cos。
  2. 检查您的算法复杂性和实现设计。
  3. 确保您使用了所有处理器必须提供的功能 - x64 架构,以及任何有用的向量指令。

【讨论】:

    【解决方案4】:

    这看起来很不错,除了 mod 操作。没有它你能做到吗?

    如果值接近于零,您可以使用

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

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

    【讨论】:

    • 我不能放弃mod操作,这是必须的。你的想法看起来很有趣,我明天会试一试,虽然我怀疑单个 mod 操作是否比多个 plus/minus 便宜,然后再次,这取决于我不太确定时需要它的时间。
    • 虽然如果 Value 已知接近 0,则单减法是一个不错的主意,但单个 mod 比 while 循环显着快。
    • 也许你可以在乘以因子后进行 mod 操作。 mod 1024*128 应该更快,因为它可以转换为按位和指令。
    【解决方案5】:

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

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

    最后,我要指出的是,至少值得研究一下整个策略是否值得。曾经,毫无疑问,使用表格来避免复杂的计算是一种可靠的策略。不过,处理器的速度比内存快了很多——以至于如今这种表查找通常是净损失。事实上,该表几乎唯一有机会的方法就是它是否足够小以适合处理器缓存。

    【讨论】:

      【解决方案6】:

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

      that,is, 用 C 表示:

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

      【讨论】:

        【解决方案7】:

        这将是非常快,因为它是。

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

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

        【讨论】:

        • 我考虑过这一点并尝试了较小的桌子,但 128x1024 几乎是收支平衡点。较小的表不会运行得更快,但较大的表开始显示速度变慢。我在 Intel 8200 Quad 上运行它
        猜你喜欢
        • 2016-06-16
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-12-23
        • 2016-06-19
        相关资源
        最近更新 更多