C++ Exp vs. Log: 哪个更快?

12

我有一个C++应用程序,需要比较两个值并决定哪个更大。唯一的复杂性在于其中一个数字以对数空间表示,另一个则不是。例如:

double log_num_1 = log(1.23);
double num_2 = 1.24;
如果我想比较num_1和num_2,我必须使用log()或exp(),我想知道哪个更容易计算(即通常运行时间更短)。您可以假设我正在使用标准cmath库。 换句话说,以下两种方法在语义上是等价的,那么哪个更快:
if(exp(log_num_1) > num_2)) cout << "num_1 is greater";
或者
if(log_num_1 > log(num_2)) cout << "num_1 is greater";

2
为什么不编写一些测试并发布您的结果呢? :) - xian
8个回答

24
据我所知,这些算法的复杂度应该是相同的,不同之处只是一个(希望可以忽略的)常数。因此,我会使用exp(a) > b,因为它不会在无效输入上中断。

3
关于log()更加脆弱的观点非常正确。尤其是在实际情况下,性能差异很可能不是一个问题。 - Michael Burr
+1,同意。除非你正在进行非常非常性能密集的操作,否则不必担心这样的微观优化... - Emil H
2
谢谢你的回答,非常有帮助。不过我会使用log()函数,因为我处理的是极小正数(概率),而且从精度上讲,这样会更好。 - Kyle Simek
1
有趣的观点,Redmoskito,结果这是一个非常有趣的问题。 - peterchen

8

编辑:修改了代码以避免exp()溢出。这导致两个函数之间的边距大大缩小。谢谢,fredrikj。

代码:

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

int main(int argc, char **argv)
{
    if (argc != 3) {
        return 0;
    }

    int type = atoi(argv[1]);
    int count = atoi(argv[2]);

    double (*func)(double) = type == 1 ? exp : log;

    int i;
    for (i = 0; i < count; i++) {
        func(i%100);
    }

    return 0;
}

(使用以下方式编译:)

emil@lanfear /home/emil/dev $ gcc -o test_log test_log.c -lm

结果似乎相当确定:

emil@lanfear /home/emil/dev $ time ./test_log 0 10000000

real    0m2.307s
user    0m2.040s
sys     0m0.000s

emil@lanfear /home/emil/dev $ time ./test_log 1 10000000

real    0m2.639s
user    0m2.632s
sys     0m0.004s

有点令人惊讶,似乎log更快。
纯属猜测:
也许是因为对数的底层数学泰勒级数收敛更快之类的原因?实际上,对我来说,自然对数指数函数更容易计算:
ln(1+x) = x - x^2/2 + x^3/3 ...
e^x = 1 + x + x^2/2! + x^3/3! + x^4/4! ...

我不确定c库函数是否确实这样做。但这似乎并不完全不可能。


1
+1:你的代码与我的不同,因为你正在评估一个变化的值,而我的代码被编译器优化成了空代码。所以错误是我的,不是你的。 - cobbal
啊,有趣。当你做同样的事情时,你得到类似的结果吗? - Emil H
4
不,指数函数的泰勒级数比自然对数函数的级数收敛得更快。但这可能不重要,因为数学库很可能使用极小值多项式而不是泰勒多项式。在您的测试中,exp比log慢得多的原因可能是它大多数时候会溢出,需要进行处理。请注意,exp(i)已经在i = 700左右时溢出了,而log则不会溢出。 - Fredrik Johansson
1
我已经修改了我的测试来弥补这个问题。感谢您提供的信息。 :) - Emil H
1
你的测试有点不公平...计算1到100之间数字的对数相当简单(它是0到~4.6之间的双精度浮点数)。然而,计算1到100之间数字的指数函数则更加困难。(exp(100)等于2.688x10^43。) - ParoXoN

8

你真的需要知道吗?这会占用你大部分的运行时间吗?你怎么知道呢?

更糟糕的是,这可能与平台有关。那么怎么办呢?

所以,如果你在意的话,可以进行测试,但花太多时间苦恼于微观优化通常不是个好主意。


2
更糟糕的是,它可能依赖于您正在运行的处理器(您的应用程序几乎肯定会比当前一代CPU更长寿)。 - Michael Kohne
我还没有进行性能分析,但这很可能是我的代码中的热点。尽管如此,你可能是对的。我只是好奇,而且我认为这可能是一个有用的问题留在这里供后人参考。 - Kyle Simek
1
好奇心从来都不是坏事。今天有太多的处理器周期被开发者浪费掉,因为他们不够好奇。"只需增加硬件"是万恶之源。 - Svante
@Svante:反对过早优化的论点不是我们可以“只需增加更多硬件”,而是(1)如果您优化了一开始就成本高昂的部分,您才能获得很多收益,(2)优化的成本超出了编写它们所需的程序员时间,以及(3)总成本通常会超过优化的总价值。因此,您首先要进行测试。现在,redmoskito指出这个问题存在一个紧密的循环中,因此它是一个很好的候选对象,但是... - dmckee --- ex-moderator kitten
@Svante:你没有抓住重点。好奇心是好的,这就是为什么在假设它是一个问题之前,你应该进行PROFILE。太多程序员的时间被浪费在那些认为他们可以猜测CPU时间花费在哪里的人身上,而才是万恶之源,因为它会从实际上能够产生差异的地方占用开发时间。首先进行分析,除非你已经确定你要优化的内容实际上需要优化,否则不要浪费时间进行优化。 - jalf

5

由于你正在处理小于1的值,注意到对于x<1,x-1>log(x),这意味着如果x-1<log(y),则log(x)<log(y),这已经涵盖了37%左右的情况,无需使用log或exp。


哪个添加的测试可能会更快,但测试和分析的方法肯定是不错的。 - David Thornley

4

一些在Python中快速测试(使用C进行数学计算):

$ time python -c "from math import log, exp;[exp(100) for i in xrange(1000000)]"

real    0m0.590s
user    0m0.520s
sys     0m0.042s

$ time python -c "from math import log, exp;[log(100) for i in xrange(1000000)]"

real    0m0.685s
user    0m0.558s
sys     0m0.044s

这表明日志可能会稍微慢一些。

编辑:编译器似乎正在优化C函数,所以循环是占用时间的部分

有趣的是,在C语言中它们似乎速度相同(可能是由于Mark在评论中提到的原因)。

#include <math.h>

void runExp(int n) {
    int i;
    for (i=0; i<n; i++) {
        exp(100);
    }
}

void runLog(int n) {
    int i;
    for (i=0; i<n; i++) {
        log(100);
    }
}

int main(int argc, char **argv) {
    if (argc <= 1) {
        return 0;
    }
    if (argv[1][0] == 'e') {
        runExp(1000000000);
    } else if (argv[1][0] == 'l') {
        runLog(1000000000);
    }
    return 0;
}

给定时间:

$ time ./exp l

real     0m2.987s
user     0m2.940s
sys      0m0.015s

$ time ./exp e 

real     0m2.988s
user     0m2.942s
sys      0m0.012s

不错的想法,但 Python 可能会在对数上执行额外的范围检查,并且它的实现方式可能与 OP 的库不同。而且,差异总是在 7% 左右吗?因为那可能只是实验误差。 - Mark

1

这取决于您的libm、平台和处理器。最好编写一些调用exp/log大量次数的代码,并使用time调用几次,以查看是否有明显差异。

在我的电脑(Windows)上,两者基本上需要相同的时间,因此我会使用exp,因为它对所有值都定义了(假设您检查了ERANGE)。但是,如果使用log更自然,则应该使用它,而不是没有充分理由地进行优化。


1

对于日志而言更快似乎是有道理的...指数函数需要执行多次乘法才能得出答案,而对数函数只需将尾数和指数从2进制转换为自然常数e的底数。

如果你使用对数函数,请务必进行边界检查(正如许多其他人所说)。


0

如果你确定这是热点代码,编译器内置函数会是你的好帮手。虽然它是平台相关的(如果你在这样的地方追求性能,你就不能成为平台无关的)。 所以,问题实际上是——在你的目标架构上,哪个是汇编指令,以及延迟和周期。没有这些信息,一切都只是纯粹的猜测。


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