使用Math.Pow的最佳实践

8
我正在开发一个图像处理库,该库扩展了OpenCV、HALCON等。该库必须使用.NET Framework 3.5,并且由于我在.NET方面的经验有限,因此我想询问一些关于性能的问题。
我遇到了一些具体的问题,自己无法很好地解释,希望您能回答以下问题:a)为什么会这样?b)如何最佳处理这些情况?
我的第一个问题是关于Math.pow。我已经在StackOverflow上找到了一些很好的解释(a),但不知道该怎么做(b)。我的基准程序如下:
Stopwatch watch = new Stopwatch();  // from the Diagnostics class
watch.Start();
for (int i = 0; i < 1000000; i++)
    double result = Math.Pow(4,7)   // the function call
watch.Stop()

结果并不理想(在我的电脑上大约需要300毫秒)(我已经运行了10次测试并计算了平均值)。

我的第一个想法是检查这是否是因为它是静态函数。所以我实现了自己的类。

class MyMath
{
    public static double Pow (double x, double y)   //Using some expensive functions to calculate the power
    {
        return Math.Exp(Math.Log(x) * y);
    }

    public static double PowLoop (double x, int y)  // Using Loop
    {
        double res = x;
        for(int i = 1; i < y; i++)
            res *= x;
        return res;
    }

    public static double Pow7 (double x)            // Using inline calls
    {
        return x * x * x * x * x * x * x;
    }
}

我检查的第三件事是,我是否可以直接通过4*4*4*4*4*4*4来替换Math.Pow(4,7)。

结果是(10次测试平均值)

300 ms   Math.Pow(4,7)
356 ms   MyMath.Pow(4,7)    //gives wrong rounded results
264 ms   MyMath.PowLoop(4,7)
 92 ms   MyMath.Pow7(4)
 16 ms   4*4*4*4*4*4*4

现在我的情况基本上是这样的: 不使用Math.Pow。我的唯一问题就是...我真的必须现在实现自己的Math-class吗?仅仅为了实现幂运算函数,似乎有点低效。(顺便说一下,PowLoop和Pow7在Release版本中甚至比Math.Pow快约25%)。

所以我的最终问题是:

a) 如果我根本不使用Math.Pow(但对于分数可能会用到),那么我错了吗?(这让我感到有点难过)。

b) 如果您要优化代码,您是否真的直接编写所有这些数学运算操作的代码?

c) 也许已经有一个更快(开源^^)的数学运算库吗?

d) 我提出这个问题的原因基本上是:我假定.NET Framework本身已经为这些基本操作提供了非常优化的代码/编译结果——无论是Math-Class还是处理数组,我有点惊讶于写自己的代码能获得多少好处。在C#中,还有其他一些通用的“领域”或其他需要注意的地方吗?


7
我认为 4444444 将在编译时计算,这就是它非常快的原因。 - Nick
1
你用更大的数字测试过吗?我认为Math.Pow是针对较大指数进行优化的,并且执行类似于:x^7 == x^{3+3+1} == {x ^ 3 + x^3 x},这意味着它的运行顺序大致是O(log(n)),而你的解决方案则是O(n),对于大指数可能会更慢。 - markijbema
1
两端测试(Pow7和静态测试)都是编译器可优化的,因此不像以前那样通用。 - TomTom
Math.Pow 应该有一个重载版本,可以接受 int 指数,因为这是最常见的情况,并且可以运行得更快。 - Richie Cotton
我相信Math类针对某些方面进行了优化/加速——无论是大数还是分形势能。但对于“非特殊”情况(例如计算Math.Pow(.., 2))或一般低整数值,编写自己的代码通常更好?在这种特定情况下,我想将我的浮点图像(1280 * 1024分辨率)计算为Image ^ 7,那么最好根本不使用.NET框架?顺便说一句,我还运行了Math.Pow(4,(int)7),结果比Math.Pow(4,7)稍慢——可能是因为转换的缘故。 - S. Richter
显示剩余3条评论
3个回答

5

需要记住的两件事:

  1. 你可能 不需要优化 这段代码。你刚刚在不到一秒钟的时间内对该函数进行了一百万次调用。这真的会在你的程序中造成大问题吗?

  2. Math.Pow 可能已经相当优化了。猜测它将调用一个用低级语言编写的适当数值库,这意味着你不应该期望数量级的增加。

  3. 数值编程比你想象的要难。即使是你认为自己知道如何计算的算法,也不是那样计算的。例如,当你计算平均值时,你不应该只是把数字加起来然后除以你拥有的数字数量。(现代数值库使用双重处理程序来纠正浮点误差。)

话虽如此,如果你确定需要优化,那么考虑使用整数而不是浮点值,或者将其外包给另一个数值库。


2
首先,整数运算比浮点数运算要快得多。如果您不需要浮点数值,请不要使用浮点数据类型。这通常适用于任何编程语言。
其次,正如您自己所述,Math.Pow可以处理实数。它使用的算法比简单循环复杂得多。难怪它比简单循环慢。如果您摆脱循环,只需进行n次乘法,就可以减少设置循环的开销-从而使其更快。但是,如果您不使用循环,则必须预先知道指数的值-无法在运行时提供。
我不确定为什么Math.Exp和Math.Log更快。但是,如果您使用Math.Log,则无法找到负值的幂。
基本上,int更快,避免循环可以避免额外的开销。但是,在选择这些内容时,您正在权衡一些灵活性。但是,如果您只需要整数,避免实数通常是一个好主意,但在这种情况下,当已经存在一个函数时编写自定义函数似乎有些过分。
您需要问自己的问题是,这是否值得。 Math.Pow是否真的拖慢了您的程序?在任何情况下,与您的语言捆绑在一起的Math.Pow通常是最快或非常接近最快的。如果您真的想制作一个通用的替代实现(即不仅限于整数,正值等),则最终可能会使用默认实现中使用的相同算法。

1
虽然我同意你回答的一般观点,但我想提出一点:当然库函数是经过优化的,但并不总是可能为所有情况进行优化。例如考虑排序,有很多有效的算法选项,它真的取决于你特定的数据哪个是最好的。 - markijbema
这就是为什么我在我的回答中使用了“通用”的词语。如果你可以对数据做出额外的假设(例如,它们总是整数,或者在给定范围内),当然你可以制作一些优化使用它(例如,计数排序而不是快速排序)。这就是我回答的全部意义,对于通用函数来说,很难与库相匹配。 - MAK

0

当你正在讨论对一行代码进行百万次迭代时,显然每一个小细节都会产生巨大的影响。

Math.Pow() 是一个函数调用,比起手动计算 4*4...*4 的例子要慢得多。

不要编写自己的类,因为很难写出比标准 Math 类更优化的东西。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接