分支预测
现代 CPU 的流水线功能使得一个指令为执行完的时候就开始执行下一条指令,但是如果遇到分支,流水线就会停止。在遇到分支的时候 CPU 会进行预测,预测正确则继续执行,预测错误则回到分支重新执行。
// 优化前#include <iostream>#include <vector>#include <algorithm>#include <random>#include <chrono>constexpr size_t COUNT = 4 * 1024 * 1024;int main () {std::default_random_engine e;std::uniform_real_distribution<double> u(0.0, 1.0);std::vector<double> vector(COUNT);std::generate(vector.begin(), vector.end(), [&e, &u]() {return u(e);});auto start_time = std::chrono::high_resolution_clock::now();double res = 0;for (int i = 0; i < COUNT; ++i) {if (vector[i] > 0.5) {res += vector[i];}}auto finish_time = std::chrono::high_resolution_clock::now();std::clog<< "sum:"<< std::chrono::duration_cast<std::chrono::microseconds>(finish_time - start_time).count()<< "us"<< std::endl;return 0;}// sum:38001us
遇到分支 vector[i] > 0.5 ,在 CPU 判断一个数字大于 0.5 之后,它会预测下一个数字也是大于 0.5 的。争对这个分支预测我们可以在创建完 vector 之后进行一次排序 std::sort(vector.begin(), vector.end()); 这样分支预测大部分时间都是正确的,除了在某个点上会错误。这样优化后的执行时间是 sum:14008us。
看出分支预测失败时对性能影响很大,我们尽量让分支预测成功,一种做法是让符合分支的和不符合分支的数分开放。实际编程要时刻注意数据和分支的对应,不要一次性去处理数据(sort)。毕竟 sort 对性能的开销也很大。
std::sort(vector.begin(), vector.end()); 可以用 std::partition(vector.begin(), vector.end(), [](double x) {return x < 0.5;}); 代替。
或者我们可以尽量避开分支 res += vector[i] > 0.5 ? vector[i] : 0; 代替分支。
如果一个分支的条件很少见 vector[i] > 0.99 我们可以加上 if (vector[i] > 0.99) [[unlikely]] {}(c++20) 来告诉编译器分支预测的概率。或者使用 __glibc_unlikely(vector[i] > 0.99) 牺牲可以移植性去提升性能。
内存访问
读取主存内存的时间往往是时钟周期的几百倍,意味着 CPU 在读取内存的 CPU 周期中,CPU 的控制单元会空闲几百个时钟周期。内存读取的特性,开销很大,批量读取(通常一次会读 64 个 字节)。

优化方法是,顺序访问优于随机访问,因为批量读取的特性,主存内存读取之后会将批量读取到的内容放到高速缓存中。
#include <iostream>#include <vector>#include <algorithm>#include <random>#include <chrono>constexpr size_t COUNT = 4 * 1024 * 1024;int main () {std::default_random_engine e;std::uniform_real_distribution<double> u(0.0, 0.99);std::vector<double> vector(COUNT);std::generate(vector.begin(), vector.end(), [&e, &u]() {return u(e);});std::vector<int> indexes(COUNT);std::iota(indexes.begin(), indexes.end(), 0);auto start_time = std::chrono::high_resolution_clock::now();double res = 0;for (int i = 0; i < COUNT; ++i) {res += vector[indexes[i]];}auto finish_time = std::chrono::high_resolution_clock::now();std::clog<< "sum:"<< res<< std::endl<< std::chrono::duration_cast<std::chrono::microseconds>(finish_time - start_time).count()<< "us"<< std::endl;return 0;}// 16999us
如果在 indexes 后 std::random_shuffle(indexes.begin(), indexes.end()); 那么最后相加时间会是 33969us 。
我们可以尽量使用同一块内存去读写,这样效率更高。
