我想要比cmath库更快的实现方法。由于我们使用的是二进制计算机,与数字2相关的许多操作可以进行位操作。
单精度浮点格式使用32位表示小数部分,第一位是符号位,接下来的8位是带有偏置值127的指数,接下来的24位编码小数部分。双精度浮点格式中,指数用11位表示,偏置值为1023,尾数为52位。
我的想法很简单,我使用乘法的幂规则,即2的a次方乘以2的b次方等于2的(a + b)次方。对于分数指数e,其小数部分d为e % 1,然后其整数部分为i = e - d。由于浮点数的编码方式,我们可以通过组合指数和尾数两部分来组装最终的浮点表示。i加上偏置值直接成为指数部分。
尾数部分有点棘手,但有一些技巧可以解决。尾数被编码为一个带有隐含整数部分为1的二进制小数,我们只需将1加到d上,就可以从浮点位得到二进制小数。注意,如果d是负数,则应该加到2而不是1。
接下来的步骤很简单,第一个位对应于2的0.5次方,第二个位对应于2的0.25次方,第三个位对应于2的0.125次方,依此类推。重要的是它们是常数,因此可以存储在查找表中。因此,对于分数幂,找到尾数的设置位,并将一个初始化为1的数字与查找表中的相应值相乘。
这给出了最终值的尾数。减去1,并将差乘以1 << 23(对于float)或1 << 52(对于double)。结果由位转换组装而成(e << p)| m。对于float,最坏情况下需要进行23次乘法,对于double,需要进行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
但仍然存在条件开销。
exp2f()
的函数,这是一个在ISO-C99中提供的示例实现,可以在这里找到。 - undefinedcpuid
进行运行时切换?您是否希望完全避免内联汇编和/或内部函数?有这么多需要回答的问题... - undefined