exp2 应该比 exp 快吗?

Should exp2 be faster than exp?

我主要对 C/C++ 中的 "exp" 和 "exp2" 函数感兴趣,但这个问题可能与 IEEE 754 标准相关,而不是特定语言功能。

在我大约 10 年前做的作业中,它试图按所需的周期对不同的浮点运算进行排序,C 函数

double exp2 (double)

似乎比

稍快
double exp (double)

鉴于"double"尾数采用二进制表示,我觉得这个结果是合理的。

然而,今天,在以几种不同的方式再次测试两者之后,我看不出有任何可衡量的差异。所以我的问题是

有许多平台不太关心他们的数学库,在这些平台上 exp2(x) 被简单地实现为 exp(x * log(2)),反之亦然。这些实现并没有提供很好的准确性(或特别好的性能),但它们相当普遍。在执行此操作的平台上,一个函数与另一个函数的成本完全相同,但需要额外乘法的成本,并且获得额外乘法的那个将是两者中较慢的一个。

在积极调整数学库并尝试提供良好准确性的平台上,这两个函数的性能非常相似。使用 exp2 可以更容易地生成结果的指数,但是获得高精度的尾数可能需要更多的工作;这两个因素大致持平,以至于性能通常在 10-15% 的范围内相当。从广义上讲,exp2 通常是两者中较快的。

  • exp2 应该(理论上)比 exp 快吗?

是的。
x86 FPU 对非整数幂执行取幂的唯一方法是使用指令 F2XM1 计算 2x-1 .
x86 上不存在 ex 指令。
x86 的任何 C 库代码都必须使用 2x 计算 expexp2


  • 应该有任何可衡量的差异吗?

没有
区别仅在于单个 FPU 乘法,这对于现代处理器来说非常快。


  • 近年来答案有变化吗?

是的。
15-20 年前,乘法的价格远高于其他运算的价格。如今,乘法几乎和加法一样便宜。

我做了一些测量,希望你们中的一些人会觉得它有用。

条件:

  • Intel(R) Xeon(R) CPU E5-2620 v2 @ 2.10GHz(服务器在测试期间有高 CPU 负载)
  • 编译器版本:g++ (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609
  • 编译器选项:-static -std=gnu++0x -ffast-math -Ofast -flto

代码:

#include <iostream>
#include <random>
#include <cmath>
#include <chrono>

using namespace std;

int main()
{
    double g = 1/log(2);
    mt19937 engine(1000);
    uniform_real_distribution<double> u(0, 1);
    double sum = 0;
    
    auto begin = std::chrono::high_resolution_clock::now();

    for (int i = 0; i < 1e7/4; ++i) // for non-parallel, `for (int i = 0; i < 1e7; ++i)`
    {
        sum += exp2(u(engine)*g);   // for exp versions, sum += exp(u(engine)); for empty versions, sum += u(engine)*g;
        sum += exp2(u(engine)*g);   // removed for non-parallel
        sum += exp2(u(engine)*g);   // removed for non-parallel
        sum += exp2(u(engine)*g);   // removed for non-parallel
    }

    auto end = std::chrono::high_resolution_clock::now();
    cout << chrono::duration_cast<chrono::nanoseconds>(end - begin).count()/1000./1000 << "ms" << "\t"
         << sum << "\t" << g << " exp2 p4" << endl;
}

执行方式: for i in {1..100}; do ./empty.bin && ./exp2_p4.bin && ./exp_p4.bin && ./exp.bin && ./exp2.bin; done

文件名告诉executable是调用exp还是exp2,求和是否按4(p4)分组。

结果

下面的 table 显示了平均运行时间(时间)、以毫秒为单位的标准偏差以及最快的情况。

|   name  | time (ms) | std (ms) | smallest (ms) |
|:-------:|:---------:|:--------:|:-------------:|
|  empty  |   244.7   |   26.2   |     130.9     |
|   exp   |   591.7   |   95.8   |     422.5     |
|   exp2  |   536.5   |   85.4   |     393.7     |
|  exp p4 |   612.3   |   89.6   |     433.2     |
| exp2 p4 |   557.2   |   87.6   |     396.8     |

一次运算需要除以1e7。我通过从指数版本中减去空版本的时间(即在不计算 exp 的情况下进行循环和求和)来估算指数的成本。这些值如下所示:

结论

exp2 在 Intel Xeon 上使用 gcc 时比 exp 快 11% 左右,即使 -ffast-math 开启,与接受的答案一致。

通过将求和分为一组四个来手动展开循环没有帮助。