exp2 应该比 exp 快吗?
Should exp2 be faster than exp?
我主要对 C/C++ 中的 "exp" 和 "exp2" 函数感兴趣,但这个问题可能与 IEEE 754 标准相关,而不是特定语言功能。
在我大约 10 年前做的作业中,它试图按所需的周期对不同的浮点运算进行排序,C 函数
double exp2 (double)
似乎比
稍快
double exp (double)
鉴于"double"尾数采用二进制表示,我觉得这个结果是合理的。
然而,今天,在以几种不同的方式再次测试两者之后,我看不出有任何可衡量的差异。所以我的问题是
- exp2 应该(理论上)比 exp 快吗?和
- 应该有任何可衡量的差异吗?和
- 近年来答案有变化吗?
有许多平台不太关心他们的数学库,在这些平台上 exp2(x)
被简单地实现为 exp(x * log(2))
,反之亦然。这些实现并没有提供很好的准确性(或特别好的性能),但它们相当普遍。在执行此操作的平台上,一个函数与另一个函数的成本完全相同,但需要额外乘法的成本,并且获得额外乘法的那个将是两者中较慢的一个。
在积极调整数学库并尝试提供良好准确性的平台上,这两个函数的性能非常相似。使用 exp2
可以更容易地生成结果的指数,但是获得高精度的尾数可能需要更多的工作;这两个因素大致持平,以至于性能通常在 10-15% 的范围内相当。从广义上讲,exp2
通常是两者中较快的。
- exp2 应该(理论上)比 exp 快吗?
是的。
x86 FPU 对非整数幂执行取幂的唯一方法是使用指令 F2XM1
计算 2x-1 .
x86 上不存在 ex 指令。
x86 的任何 C 库代码都必须使用 2x 计算 exp
和 exp2
。
- 应该有任何可衡量的差异吗?
没有
区别仅在于单个 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
开启,与接受的答案一致。
通过将求和分为一组四个来手动展开循环没有帮助。
我主要对 C/C++ 中的 "exp" 和 "exp2" 函数感兴趣,但这个问题可能与 IEEE 754 标准相关,而不是特定语言功能。
在我大约 10 年前做的作业中,它试图按所需的周期对不同的浮点运算进行排序,C 函数
double exp2 (double)
似乎比
稍快double exp (double)
鉴于"double"尾数采用二进制表示,我觉得这个结果是合理的。
然而,今天,在以几种不同的方式再次测试两者之后,我看不出有任何可衡量的差异。所以我的问题是
- exp2 应该(理论上)比 exp 快吗?和
- 应该有任何可衡量的差异吗?和
- 近年来答案有变化吗?
有许多平台不太关心他们的数学库,在这些平台上 exp2(x)
被简单地实现为 exp(x * log(2))
,反之亦然。这些实现并没有提供很好的准确性(或特别好的性能),但它们相当普遍。在执行此操作的平台上,一个函数与另一个函数的成本完全相同,但需要额外乘法的成本,并且获得额外乘法的那个将是两者中较慢的一个。
在积极调整数学库并尝试提供良好准确性的平台上,这两个函数的性能非常相似。使用 exp2
可以更容易地生成结果的指数,但是获得高精度的尾数可能需要更多的工作;这两个因素大致持平,以至于性能通常在 10-15% 的范围内相当。从广义上讲,exp2
通常是两者中较快的。
- exp2 应该(理论上)比 exp 快吗?
是的。
x86 FPU 对非整数幂执行取幂的唯一方法是使用指令 F2XM1
计算 2x-1 .
x86 上不存在 ex 指令。
x86 的任何 C 库代码都必须使用 2x 计算 exp
和 exp2
。
- 应该有任何可衡量的差异吗?
没有
区别仅在于单个 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
开启,与接受的答案一致。
通过将求和分为一组四个来手动展开循环没有帮助。