提问人:Ξένη Γήινος 提问时间:9/27/2023 最后编辑:Ξένη Γήινος 更新时间:9/28/2023 访问量:282
如何在 C++ 中有效地将 2 提高到小数次幂?
How to efficiently raise 2 to a fractional power in C++?
问:
我想有效地将两个提升到理性的力量。这将是我所有其他数学函数实现()的基础,因为我使用牛顿方法,并且迭代方案的负载涉及幂。log, log2, exp, sin, cos
我想实现比 cmath 更快的方法。因为我们使用二进制计算机,所以很多与数字二相关的东西都可能被黑客入侵。
单精度浮点格式使用 32 位表示分数,第一位是符号位,接下来的 8 位是偏置为 127 的指数,接下来的 24 位对分数进行编码。对于双精度,指数编码为 11 位,偏差为 1023,尾数为 52 位。
我的想法很简单,我使用乘法的幂法则,在这种情况下是 2 a * 2 b = 2a + b。对于分数指数 e,其小数部分 d 为 e % 1,然后其积分部分是 i = e - d。因为浮点的编码方式,我们可以通过组合指数和尾数两部分来组装最终的浮点表示。我带着它的偏差直接进入指数部分。
尾数部分有点棘手,但有一些技巧。尾数被编码为二进制小数,隐式整数部分是 1,我们只需将 1 添加到 d 即可从浮点位中获取二进制分数。请注意,如果 d 为负数,则应将其添加到 2 而不是 1。
接下来的步骤很简单,第一位对应于 2 0.5,第二位对应于 2 0.25,对应于第三位 20.125,依此类推。重要的是它们是常量,因此它们可以存储在查找表中。因此,对于小数幂,找到尾数的设定位,并将初始化为 1 的数字乘以查找表中的相应值。
这给出了最终值的尾数。减去 1 并将差值乘以 1 << 23 表示浮点数,1 << 52 表示双倍数。结果由钻头铸造组装而成 (e << p) |对于浮点数,最坏的情况是 23 次乘法,对于双精度,最坏情况是 52 次乘法。
我的代码:
#include <array>
#include <bitset>
#include <chrono>
#include <cmath>
#include <iostream>
#include <utility>
#include <vector>
using std::array;
using std::chrono::steady_clock;
using std::chrono::duration;
using std::cout;
using std::vector;
float r = 0.0;
const array<double, 52> EXP2_BITS = [] {
array<double, 52> exp2a;
for (int i = 1; i < 53; i++) {
exp2a[i - 1] = exp2(1.0 / (int64_t(1) << i));
}
return exp2a;
}();
constexpr int F52 = 1 << 23;
inline float fast_exp2_f(float f) {
float frac = fmodf(f, 1.0f);
int i, e;
e = f - frac + 127;
frac += frac >= 0 ? 1 : 2;
uint32_t bits = std::bit_cast<uint32_t>(frac);
uint32_t m = bits & 0x7fffff;
i = 22;
double p = 1.0;
while (m) {
if (m & 1) {
p *= EXP2_BITS[i];
}
m >>= 1;
i--;
}
uint32_t m1 = (p - 1.0) * F52;
return std::bit_cast<float>((e << 23) | m1);
}
template <size_t N, size_t... I>
inline double exp2_helper_impl(std::bitset<N> m, std::index_sequence<I...>) {
double p = 1.0;
((p *= std::get<I>(m)), ...);
return p;
}
template <std::size_t N>
inline double exp2_helper(const std::bitset<N>& m) {
return exp2_helper_impl(m, std::make_index_sequence<N>{});
}
inline float fast_exp2_f1(float f) {
float frac = fmodf(f, 1.0f);
int i, e;
e = f - frac + 127;
frac += frac >= 0 ? 1 : 2;
uint32_t bits = std::bit_cast<uint32_t>(frac);
std::bitset<23> m(bits & 0x7fffff);
double p = exp2_helper(m);
uint32_t m1 = (p - 1.0) * F52;
return std::bit_cast<float>((e << 23) | m1);
}
int main()
{
cout << std::setprecision(7);
vector<float> bases(256);
vector<int> ns(256);
float r256 = 1.0 / 256;
for (int i = 0; i < 256; i++) {
bases[i] = 1.0 + rand() % 120 + (rand() % 256) * r256;
ns[i] = 2 + rand() % 30;
}
auto start = steady_clock::now();
for (int64_t i = 0; i < 1048576; i++) {
r += fast_exp2_f(bases[i % 256]);
}
auto end = steady_clock::now();
duration<double, std::nano> time = end - start;
cout << "fast_exp2_f: " << time.count() / 1048576 << " nanoseconds\n";
start = steady_clock::now();
for (int64_t i = 0; i < 1048576; i++) {
r += exp2f(bases[i % 256]);
}
end = steady_clock::now();
time = end - start;
cout << "exp2f: " << time.count() / 1048576 << " nanoseconds\n";
start = steady_clock::now();
for (int64_t i = 0; i < 1048576; i++) {
r += fast_exp2_f1(bases[i % 256]);
}
end = steady_clock::now();
time = end - start;
cout << "fast_exp2_f1: " << time.count() / 1048576 << " nanoseconds\n";
float n;
for (int i = 0; i < 256; i++) {
n = bases[i];
cout << "n: " << n << ", fast_exp2_f: " << fast_exp2_f(n) << ", fast_exp2_f1: " << fast_exp2_f1(n) << ", exp2f: " << exp2f(n) << '\n';
}
}
我的方法有效并且非常准确,但速度较慢:
fast_exp2_f: 57.74498 nanoseconds
exp2f: 36.58924 nanoseconds
n: 42.13672, fast_exp2_f: 4.83522e+12, exp2f: 4.83522e+12
n: 101.8789, fast_exp2_f: 4.66237e+30, exp2f: 4.66237e+30
n: 79.67969, fast_exp2_f: 9.682243e+23, exp2f: 9.682243e+23
n: 105.2852, fast_exp2_f: 4.942994e+31, exp2f: 4.942994e+31
n: 2.730469, fast_exp2_f: 6.636712, exp2f: 6.636713
n: 12.69922, fast_exp2_f: 6650.369, exp2f: 6650.369
n: 28.23438, fast_exp2_f: 3.157867e+08, exp2f: 3.157867e+08
n: 85.24219, fast_exp2_f: 4.575677e+25, exp2f: 4.575677e+25
n: 53.36719, fast_exp2_f: 1.161781e+16, exp2f: 1.161781e+16
n: 117.0234, fast_exp2_f: 1.688748e+35, exp2f: 1.688748e+35
n: 48.86719, fast_exp2_f: 5.134394e+14, exp2f: 5.134394e+14
n: 19.30078, fast_exp2_f: 645823.8, exp2f: 645823.9
n: 108.7305, fast_exp2_f: 5.384341e+32, exp2f: 5.384341e+32
n: 55.12109, fast_exp2_f: 3.918344e+16, exp2f: 3.918345e+16
n: 3.488281, fast_exp2_f: 11.22218, exp2f: 11.22218
n: 105.1445, fast_exp2_f: 4.483919e+31, exp2f: 4.483919e+31
n: 54.82812, fast_exp2_f: 3.198234e+16, exp2f: 3.198234e+16
n: 45.58594, fast_exp2_f: 5.281223e+13, exp2f: 5.281224e+13
n: 118.2305, fast_exp2_f: 3.898679e+35, exp2f: 3.898679e+35
n: 22.53516, fast_exp2_f: 6077962, exp2f: 6077962
n: 77.85547, fast_exp2_f: 2.734207e+23, exp2f: 2.734207e+23
n: 43.125, fast_exp2_f: 9.592207e+12, exp2f: 9.592208e+12
n: 41.92969, fast_exp2_f: 4.188839e+12, exp2f: 4.188839e+12
n: 89.21094, fast_exp2_f: 7.164207e+26, exp2f: 7.164207e+26
n: 51.28516, fast_exp2_f: 2.743913e+15, exp2f: 2.743913e+15
n: 111.6172, fast_exp2_f: 3.982185e+33, exp2f: 3.982185e+33
n: 34.85938, fast_exp2_f: 3.116861e+10, exp2f: 3.116861e+10
n: 24.07812, fast_exp2_f: 1.771079e+07, exp2f: 1.771079e+07
n: 37.25, fast_exp2_f: 1.634434e+11, exp2f: 1.634434e+11
n: 57.41797, fast_exp2_f: 1.925444e+17, exp2f: 1.925444e+17
n: 25.71484, fast_exp2_f: 5.507307e+07, exp2f: 5.507307e+07
n: 44.62891, fast_exp2_f: 2.720442e+13, exp2f: 2.720442e+13
n: 39.13281, fast_exp2_f: 6.027682e+11, exp2f: 6.027683e+11
n: 102.8789, fast_exp2_f: 9.324739e+30, exp2f: 9.324739e+30
n: 80.85156, fast_exp2_f: 2.181451e+24, exp2f: 2.181451e+24
n: 91.59766, fast_exp2_f: 3.746641e+27, exp2f: 3.746641e+27
n: 114.4453, fast_exp2_f: 2.827951e+34, exp2f: 2.827951e+34
n: 66.17188, fast_exp2_f: 8.312262e+19, exp2f: 8.312262e+19
n: 31.76953, fast_exp2_f: 3.660849e+09, exp2f: 3.660849e+09
n: 94.91016, fast_exp2_f: 3.722236e+28, exp2f: 3.722236e+28
n: 107.9141, fast_exp2_f: 3.057523e+32, exp2f: 3.057523e+32
n: 37.32422, fast_exp2_f: 1.720717e+11, exp2f: 1.720717e+11
n: 16.83594, fast_exp2_f: 116982.8, exp2f: 116982.9
所以我想优化它。我认为性能瓶颈是 while 循环,我已经看到使用带有内联的索引序列来展开循环可以使代码在不到 2 纳秒的时间内运行,而使用 while 循环将整数转换为二进制是一种非常低效的方式,整数已经在内部存储为二进制, 因此,我们应该直接访问二进制文件。
我尝试将索引序列与位集一起使用,但没有奏效。它会导致编译失败。错误信息:
D:\MyScript\CodeBlocks\testapp\main.cpp:103:23: error: no matching function for call to 'get<0>(std::bitset<23>&)'
我的第一个方法有效,我用以下方法编译它:
g++.exe -Wall -fexceptions -fomit-frame-pointer -fexpensive-optimizations -flto -O3 -m64 --std=c++20 -march=native -ffast-math -c D:\MyScript\CodeBlocks\testapp\main.cpp -o obj\Release\main.o
g++.exe -o bin\Release\testapp.exe obj\Release\main.o -O3 -flto -s -static-libstdc++ -static-libgcc -static -m64
我该如何优化它?
无论它的价值如何,我都手动展开了循环,它确实使代码更快,正如预期的那样,但它很丑陋,效率也不高:
inline float fast_exp2_f1(float f) {
int e = int(f);
float frac = f - e;
e += 127;
frac += frac >= 0 ? 1 : 2;
uint32_t bits = std::bit_cast<uint32_t>(frac);
std::bitset<23> m(bits & 0x7fffff);
double p = 1.0;
if (m[0]) {p *= EXP2_BITS[22]; }
if (m[1]) {p *= EXP2_BITS[21]; }
if (m[2]) {p *= EXP2_BITS[20]; }
if (m[3]) {p *= EXP2_BITS[19]; }
if (m[4]) {p *= EXP2_BITS[18]; }
if (m[5]) {p *= EXP2_BITS[17]; }
if (m[6]) {p *= EXP2_BITS[16]; }
if (m[7]) {p *= EXP2_BITS[15]; }
if (m[8]) {p *= EXP2_BITS[14]; }
if (m[9]) {p *= EXP2_BITS[13]; }
if (m[10]) {p *= EXP2_BITS[12]; }
if (m[11]) {p *= EXP2_BITS[11]; }
if (m[12]) {p *= EXP2_BITS[10]; }
if (m[13]) {p *= EXP2_BITS[9]; }
if (m[14]) {p *= EXP2_BITS[8]; }
if (m[15]) {p *= EXP2_BITS[7]; }
if (m[16]) {p *= EXP2_BITS[6]; }
if (m[17]) {p *= EXP2_BITS[5]; }
if (m[18]) {p *= EXP2_BITS[4]; }
if (m[19]) {p *= EXP2_BITS[3]; }
if (m[20]) {p *= EXP2_BITS[2]; }
if (m[21]) {p *= EXP2_BITS[1]; }
if (m[22]) {p *= EXP2_BITS[0]; }
uint32_t m1 = (p - 1.0) * F23;
return std::bit_cast<float>((e << 23) | m1);
}
fast_exp2_f: 50.11549 nanoseconds
exp2f: 36.29322 nanoseconds
fast_exp2_f1: 11.36074 nanoseconds
我现在已经将我的代码转换为无分支形式:
const array<double, 104> EXP2_BITS = [] {
array<double, 104> exp2a;
for (int i = 1; i < 53; i++) {
exp2a[i * 2 - 2] = 1.0;
exp2a[i * 2 - 1] = exp2(1.0 / (int64_t(1) << i));
}
return exp2a;
}();
constexpr int F23 = 1 << 23;
inline float fast_exp2_f1(float f) {
int e = int(f);
float frac = f - e;
e += 127;
frac += frac >= 0 ? 1 : 2;
uint32_t bits = std::bit_cast<uint32_t>(frac);
std::bitset<23> m(bits & 0x7fffff);
double p = 1.0;
p *= EXP2_BITS[44+m[0]];
p *= EXP2_BITS[42+m[1]];
p *= EXP2_BITS[40+m[2]];
p *= EXP2_BITS[38+m[3]];
p *= EXP2_BITS[36+m[4]];
p *= EXP2_BITS[34+m[5]];
p *= EXP2_BITS[32+m[6]];
p *= EXP2_BITS[30+m[7]];
p *= EXP2_BITS[28+m[8]];
p *= EXP2_BITS[26+m[9]];
p *= EXP2_BITS[24+m[10]];
p *= EXP2_BITS[22+m[11]];
p *= EXP2_BITS[20+m[12]];
p *= EXP2_BITS[18+m[13]];
p *= EXP2_BITS[16+m[14]];
p *= EXP2_BITS[14+m[15]];
p *= EXP2_BITS[12+m[16]];
p *= EXP2_BITS[10+m[17]];
p *= EXP2_BITS[8+m[18]];
p *= EXP2_BITS[6+m[19]];
p *= EXP2_BITS[4+m[20]];
p *= EXP2_BITS[2+m[21]];
p *= EXP2_BITS[0+m[22]];
uint32_t m1 = (p - 1.0) * F23;
return std::bit_cast<float>((e << 23) | m1);
}
它应该更稳定,并消除了条件开销。但是每个位都有乘法赋值开销,因此代码需要更长的时间才能执行。
fast_exp2_f1: 20.96491 nanoseconds
我已经做到了。我已经打败了。exp2f
const array<double, 52> EXP2_BITS = [] {
array<double, 52> exp2a;
for (int i = 1; i < 53; i++) {
exp2a[i - 1] = exp2(1.0 / (int64_t(1) << i));
}
return exp2a;
}();
constexpr int F23 = 1 << 23;
inline float fast_exp2_f(float f) {
int e = int(f);
float frac = f - e;
e += 127;
frac += frac >= 0 ? 1 : 2;
uint32_t bits = std::bit_cast<uint32_t>(frac);
std::bitset<23> m(bits & 0x7fffff);
double p = 1.0;
p *= m[0] ? EXP2_BITS[22] : 1;
p *= m[1] ? EXP2_BITS[21] : 1;
p *= m[2] ? EXP2_BITS[20] : 1;
p *= m[3] ? EXP2_BITS[19] : 1;
p *= m[4] ? EXP2_BITS[18] : 1;
p *= m[5] ? EXP2_BITS[17] : 1;
p *= m[6] ? EXP2_BITS[16] : 1;
p *= m[7] ? EXP2_BITS[15] : 1;
p *= m[8] ? EXP2_BITS[14] : 1;
p *= m[9] ? EXP2_BITS[13] : 1;
p *= m[10] ? EXP2_BITS[12] : 1;
p *= m[11] ? EXP2_BITS[11] : 1;
p *= m[12] ? EXP2_BITS[10] : 1;
p *= m[13] ? EXP2_BITS[9] : 1;
p *= m[14] ? EXP2_BITS[8] : 1;
p *= m[15] ? EXP2_BITS[7] : 1;
p *= m[16] ? EXP2_BITS[6] : 1;
p *= m[17] ? EXP2_BITS[5] : 1;
p *= m[18] ? EXP2_BITS[4] : 1;
p *= m[19] ? EXP2_BITS[3] : 1;
p *= m[20] ? EXP2_BITS[2] : 1;
p *= m[21] ? EXP2_BITS[1] : 1;
p *= m[22] ? EXP2_BITS[0] : 1;
uint32_t m1 = (p - 1.0) * F23;
return std::bit_cast<float>((e << 23) | m1);
}
即使数组中有 32768 个随机值,上述操作仍然更快。
fast_exp2_f: 11.4665 nanoseconds
exp2f: 36.11965 nanoseconds
但仍然存在条件开销。
答:
TL;DR:实现受 GCC 上依赖条件跳转链的约束。Clang 生成更好的汇编代码(感谢 SIMD 指令和无分支代码)。该基准显然是有偏见的。与 SIMD 实现相比,标量实现效率低下,因此如果可能,应使用后者。exp2f
分析
主要问题是依赖条件指令的长链。事实上,这是使用 GCC 生成的代码的一部分:fast_exp2_f1
[...]
.L33:
test ah, 16
je .L34
vmulsd xmm0, xmm0, QWORD PTR .LC21[rip]
.L34:
test ah, 32
je .L35
vmulsd xmm0, xmm0, QWORD PTR .LC22[rip]
.L35:
test ah, 64
je .L36
vmulsd xmm0, xmm0, QWORD PTR .LC23[rip]
.L36:
test ah, -128
je .L37
vmulsd xmm0, xmm0, QWORD PTR .LC24[rip]
.L37:
test eax, 65536
je .L38
vmulsd xmm0, xmm0, QWORD PTR .LC25[rip]
.L38:
test eax, 131072
je .L39
vmulsd xmm0, xmm0, QWORD PTR .LC26[rip]
.L39:
test eax, 262144
je .L40
vmulsd xmm0, xmm0, QWORD PTR .LC27[rip]
[...]
处理器需要评估是否进行条件跳转,然后相应地跳转。当进行条件跳转时,处理器不能比 1 个周期更快。问题是你有 23 个条件跳跃,所以如果全部跳转,它不能超过 23 个周期。在我的 i5-9600KF 处理器上,这意味着至少 5.11 ns 仅适用于一系列条件。如果不进行跳转,则 FMA 延迟是瓶颈,因为在大多数主流机器上,双精度乘法通常需要大约 2-8 个周期。在我的 i5-9600KF 处理器上,它需要 4 个周期。因此,如果未执行所有条件跳转,则代码至少需要周期,因为所有乘法运算都是相互依赖的。请注意,由于双精度数字的非关联性,处理器无法对它们进行重新排序,虽然编译器理论上可以对它们进行重新排序,但由于条件的原因,这对他们来说太复杂了。在实践中,有条件跳跃和未有条件跳跃的混合,因此,只有在未进行多次条件跳跃时,FMA 延迟才是一个问题。事实证明,大多数条件跳跃都是在基准测试中进行的,但不是全部。事实上,平均而言,没有进行 4 次有条件的跳跃,而所有其他的都有。这意味着任何有条件的跳跃都有 83% 的机会被采取。23*4=92
条件跳跃的成本是可变的,难以预测。当进行条件跳转时,首先需要从内存中不同的非连续位置获取和解码要执行的指令。问题是结果取决于管道其他部分产生的数据。由于管道停滞(通常为 >10 个周期),这可能会带来显着的开销。现代主流处理器非常复杂,它们是由聪明的工程师和研究人员设计了几十年,他们试图尽可能地减少这种开销。主流处理器现在预测条件跳跃,以减少管道停顿。当一个条件被很好地预测时,在最近的主流处理器上通常(几乎)没有管道停顿。当出现预测失误时,开销可能会很大,尤其是由于条件跳跃的长链。
事实证明,我的处理器(就像最近的主流处理器一样)能够预测许多条件跳跃。事实上,它能够根据正在计算的实际数字来知道是否进行了跳跃。嗯,这是一个巨大的简化,因为实践中发生的事情要复杂得多(分支预测是一个非常复杂的话题)。事实上,双精度数字的输入数组中只有 256 个数字,因此处理器可以记住 256 个数字的大多数条件跳跃的结果,以便成功预测它们(顺便说一句,这非常疯狂)。简而言之,这意味着基准测试由于处理器优化而存在偏差。这可以通过将输入数组中的项目数从 256 增加到 32768 来在我的机器上看到(使用 Clang 快速检查,而无需在 Windows 上生成接近 GCC 的代码):-ffast-math
256 items:
- fast_exp2_f: 44.950962 nanoseconds
- exp2f: 40.409470 nanoseconds
- fast_exp2_f1: 13.479328 nanoseconds
32768 items:
- fast_exp2_f: 68.788147 nanoseconds
- exp2f: 40.300083 nanoseconds
- fast_exp2_f1: 45.244217 nanoseconds
可以看出,这不受输入数组中项目数量的影响。这是因为其中要么没有条件跳跃,要么很少有被很好地预测的跳跃(即与基准测试中使用的实际输入数字没有太大关系)。最后,在更现实的用例中,fast_exp2_f1
实际上比 exp2f
慢。事实上,输入数字往往彼此接近,因此比 具有优势。使用范围更广的输入随机数应该会减慢速度。在我的机器上,它应该花费超过 60 ns 的时间。exp2f
fast_exp2_f1
exp2f
fast_exp2_f1
如何使代码更快
通常,避免分支错误预测问题的简单解决方案是编写无分支代码。这在 x86-64 处理器上是可能的,这要归功于 SIMD (SSE) 指令,例如 .然而,这并不是一个好的解决方案,因为大多数条件跳跃都是采用的,因此大多数乘法都不会执行。使用朴素的无分支代码会导致 FMA 延迟成为一个问题(这部分代码在我的机器上至少需要 20 ns)。blendpd
更聪明的解决方案是将乘法与混合相结合进行二进制约简。自己做这件事很乏味,不便携和低级。事实上,GCC 倾向于不生成无分支代码,而 AFAIK 没有办法使用可移植代码强制它。因此,SIMD (SSE) 内部函数是进行此类优化的方法。事实上,可以使用 SIMD 指令一次对多个数字进行操作。事实证明,Clang 用 -O3 -ffast-math
做得很好。以下是 Clang 生成的部分代码:
vpand ymm6, ymm0, ymmword ptr [rip + .LCPI1_5]
vpand ymm7, ymm0, ymmword ptr [rip + .LCPI1_6]
vpand ymm8, ymm0, ymmword ptr [rip + .LCPI1_7]
vpand ymm9, ymm0, ymmword ptr [rip + .LCPI1_8]
vpcmpeqq ymm9, ymm9, ymm3
vpcmpeqq ymm7, ymm7, ymm3
vpcmpeqq ymm6, ymm6, ymm3
vmovupd xmm10, xmmword ptr [rip + EXP2_BITS+168]
vunpcklpd xmm5, xmm10, xmm5 # xmm5 = xmm10[0],xmm5[0]
vpermpd ymm10, ymmword ptr [rip + EXP2_BITS+152], 20 # ymm10 = mem[0,1,1,0]
vblendpd ymm5, ymm5, ymm10, 12 # ymm5 = ymm5[0,1],ymm10[2,3]
vblendvpd ymm5, ymm5, ymmword ptr [rip + .LCPI1_9], ymm7
vpermpd ymm7, ymmword ptr [rip + EXP2_BITS+120], 27 # ymm7 = mem[3,2,1,0]
vpermpd ymm10, ymmword ptr [rip + EXP2_BITS+88], 27 # ymm10 = mem[3,2,1,0]
vblendvpd ymm6, ymm10, ymm4, ymm6
vpermpd ymm10, ymmword ptr [rip + EXP2_BITS+56], 27 # ymm10 = mem[3,2,1,0]
vpcmpeqq ymm3, ymm8, ymm3
vblendvpd ymm3, ymm10, ymm4, ymm3
vblendvpd ymm4, ymm7, ymm4, ymm9
vmulpd ymm3, ymm4, ymm3
vmulpd ymm3, ymm6, ymm3
vmulpd ymm3, ymm5, ymm3
vextractf128 xmm4, ymm3, 1
vmulpd xmm3, xmm3, xmm4
vshufpd xmm4, xmm3, xmm3, 1 # xmm4 = xmm3[1,0]
vmulsd xmm3, xmm3, xmm4
vextractf128 xmm4, ymm2, 1
vmulpd xmm2, xmm2, xmm4
vshufpd xmm4, xmm2, xmm2, 1 # xmm4 = xmm2[1,0]
vmulsd xmm2, xmm2, xmm4
vpand xmm0, xmm0, xmmword ptr [rip + .LCPI1_10]
vxorpd xmm4, xmm4, xmm4
vpcmpeqq xmm0, xmm0, xmm4
vmovupd xmm4, xmmword ptr [rip + EXP2_BITS+8]
vblendvpd xmm0, xmm4, xmmword ptr [rip + .LCPI1_11], xmm0
可以注意到一次对多个值的操作,以无分支的方式执行条件。此代码相对较快,并且与输入值无关:vmulpd
vpand
vpcmpeqq
vblendvpd
256 items:
- fast_exp2_f: 43.608665 nanoseconds
- exp2f: 40.797138 nanoseconds
- fast_exp2_f1: 15.594292 nanoseconds
32768 items:
- fast_exp2_f: 70.216560 nanoseconds
- exp2f: 41.187191 nanoseconds
- fast_exp2_f1: 16.847897 nanoseconds
可以看出,时序不受输入数组大小的影响。事实上,比 .fast_exp2_f1
fast_exp2_f1
exp2f
简而言之:如果可以的话,在这里使用 Clang。
笔记
在这种情况下,所有实现都是低效的。您可以使用 SIMD 指令一次对多个输入值进行操作。这比 Clang 所做的使用更有效(即矢量化函数,该函数一次仍对 1 个输入值进行操作)。大多数优化的数学库都可以做到这一点(如英特尔 SVML 或 ISPC,甚至是 PS4/PS5 上索尼的标准数学库)。我希望这种优化的库在我的机器上明显快于 10 ns/项。但是,它们通常会引入很少的 ULP 的小误差。这在您的情况下应该不是问题,因为您无论如何都会使用。fast_exp2_f1
-ffast-math
-ffast-math
只是为了好玩,我尝试在您的基准测试中使用 ISPC。看起来 ISPC 没有实现,但可能比直接计算慢(因为经常计算 a 和 an,但我不确定常数基数是否为 2)。正如预期的那样,ISPC 比基准测试中的所有实现都要快得多(精度为 >=5 位)。它比我机器上的其他实现快 >30 倍(这要归功于 AVX-2 SIMD 指令集和积极优化的无分支计算):exp2f
pow
exp2f
pow
log
exp
ISPC pow(2.0f,x): 0.417328 nanoseconds
我建议你不要重新发明轮子,使用 SIMD 实现是可能的,并阅读有关它的研究论文。实现比优化的数学库更快的指数函数,同时提供至少相同的精度是非常困难的,即使对于技术娴熟的开发人员也是如此。
评论
exp2f()
cpuid