为什么我的带有 C++20 likely/unlikely 属性的代码没有更快?
Why isn’t my code with C++20 likely/unlikely attributes faster?
在 Visual Studio 2019 版本 16.11.8 上使用 /O2 优化和英特尔 CPU 编写代码 运行。我试图找到这个反直觉结果的根本原因我得到的是没有属性在统计上比通过 t 测试有属性更快。我不确定这是什么根本原因。它可能是某种缓存吗?或者编译器正在做的一些魔术 - 我无法真正阅读汇编
#include <chrono>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <random>
#include <vector>
#include <cmath>
#include <functional>
static const size_t NUM_EXPERIMENTS = 1000;
double calc_mean(std::vector<double>& vec) {
double sum = 0;
for (auto& x : vec)
sum += x;
return sum / vec.size();
}
double calc_deviation(std::vector<double>& vec) {
double sum = 0;
for (int i = 0; i < vec.size(); i++)
sum = sum + (vec[i] - calc_mean(vec)) * (vec[i] - calc_mean(vec));
return sqrt(sum / (vec.size()));
}
double calc_ttest(std::vector<double> vec1, std::vector<double> vec2){
double mean1 = calc_mean(vec1);
double mean2 = calc_mean(vec2);
double sd1 = calc_deviation(vec1);
double sd2 = calc_deviation(vec2);
double t_test = (mean1 - mean2) / sqrt((sd1 * sd1) / vec1.size() + (sd2 * sd2) / vec2.size());
return t_test;
}
namespace with_attributes {
double calc(double x) noexcept {
if (x > 2) [[unlikely]]
return sqrt(x);
else [[likely]]
return pow(x, 2);
}
} // namespace with_attributes
namespace no_attributes {
double calc(double x) noexcept {
if (x > 2)
return sqrt(x);
else
return pow(x, 2);
}
} // namespace with_attributes
std::vector<double> benchmark(std::function<double(double)> calc_func) {
std::vector<double> vec;
vec.reserve(NUM_EXPERIMENTS);
std::mt19937 mersenne_engine(12);
std::uniform_real_distribution<double> dist{ 1, 2.2 };
for (size_t i = 0; i < NUM_EXPERIMENTS; i++) {
const auto start = std::chrono::high_resolution_clock::now();
for (auto size{ 1ULL }; size != 100000ULL; ++size) {
double x = dist(mersenne_engine);
calc_func(x);
}
const std::chrono::duration<double> diff =
std::chrono::high_resolution_clock::now() - start;
vec.push_back(diff.count());
}
return vec;
}
int main() {
std::vector<double> vec1 = benchmark(with_attributes::calc);
std::vector<double> vec2 = benchmark(no_attributes::calc);
std::cout << "with attribute: " << std::fixed << std::setprecision(6) << calc_mean(vec1) << '\n';
std::cout << "without attribute: " << std::fixed << std::setprecision(6) << calc_mean(vec2) << '\n';
std::cout << "T statistics" << std::fixed << std::setprecision(6) << calc_ttest(vec1, vec2) << '\n';
}
根据 godbolt,这两个函数在 msvc
下生成相同的程序集
movsd xmm1, QWORD PTR __real@4000000000000000
comisd xmm0, xmm1
jbe SHORT $LN2@calc
xorps xmm1, xmm1
ucomisd xmm1, xmm0
ja SHORT $LN7@calc
sqrtsd xmm0, xmm0
ret 0
$LN7@calc:
jmp sqrt
$LN2@calc:
jmp pow
由于msvc不是开源编译器,只能猜测为什么msvc会选择忽略这个优化——可能因为两个分支都是函数调用(是尾调用所以jmp
而不是call
),这对 [[likely]] 来说太昂贵了,无法改变现状。
如果编译器改成clang,它会很聪明地把power 2优化成x * x,所以会生成不同的代码。按照这个思路,如果你的代码修改成
double calc(double x) noexcept {
if (x > 2)
return x + 1;
else
return x - 2;
}
msvc 也会输出不同的布局。
编译器很聪明。这些天,他们非常聪明。他们做了很多工作来弄清楚什么时候需要做事。
likely 和 unlikely 属性的存在是为了解决极其的具体问题。只有在对特定 performance-critical 代码的性能特征和生成的程序集进行深入分析后,问题才会变得明显。它们不是您在任何旧代码中涂抹以使其运行速度更快的药膏。
他们是手术刀。而且如果没有接受过手术训练,手术刀很可能会被误用。
因此,除非您对汇编分析显示可以通过更好的分支预测来解决的性能问题有特定的了解,否则您不应假设 任何 使用这些属性会使任何特定的代码都运行得更快。
也就是说,您得到的结果是完全合法的。
在 Visual Studio 2019 版本 16.11.8 上使用 /O2 优化和英特尔 CPU 编写代码 运行。我试图找到这个反直觉结果的根本原因我得到的是没有属性在统计上比通过 t 测试有属性更快。我不确定这是什么根本原因。它可能是某种缓存吗?或者编译器正在做的一些魔术 - 我无法真正阅读汇编
#include <chrono>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <random>
#include <vector>
#include <cmath>
#include <functional>
static const size_t NUM_EXPERIMENTS = 1000;
double calc_mean(std::vector<double>& vec) {
double sum = 0;
for (auto& x : vec)
sum += x;
return sum / vec.size();
}
double calc_deviation(std::vector<double>& vec) {
double sum = 0;
for (int i = 0; i < vec.size(); i++)
sum = sum + (vec[i] - calc_mean(vec)) * (vec[i] - calc_mean(vec));
return sqrt(sum / (vec.size()));
}
double calc_ttest(std::vector<double> vec1, std::vector<double> vec2){
double mean1 = calc_mean(vec1);
double mean2 = calc_mean(vec2);
double sd1 = calc_deviation(vec1);
double sd2 = calc_deviation(vec2);
double t_test = (mean1 - mean2) / sqrt((sd1 * sd1) / vec1.size() + (sd2 * sd2) / vec2.size());
return t_test;
}
namespace with_attributes {
double calc(double x) noexcept {
if (x > 2) [[unlikely]]
return sqrt(x);
else [[likely]]
return pow(x, 2);
}
} // namespace with_attributes
namespace no_attributes {
double calc(double x) noexcept {
if (x > 2)
return sqrt(x);
else
return pow(x, 2);
}
} // namespace with_attributes
std::vector<double> benchmark(std::function<double(double)> calc_func) {
std::vector<double> vec;
vec.reserve(NUM_EXPERIMENTS);
std::mt19937 mersenne_engine(12);
std::uniform_real_distribution<double> dist{ 1, 2.2 };
for (size_t i = 0; i < NUM_EXPERIMENTS; i++) {
const auto start = std::chrono::high_resolution_clock::now();
for (auto size{ 1ULL }; size != 100000ULL; ++size) {
double x = dist(mersenne_engine);
calc_func(x);
}
const std::chrono::duration<double> diff =
std::chrono::high_resolution_clock::now() - start;
vec.push_back(diff.count());
}
return vec;
}
int main() {
std::vector<double> vec1 = benchmark(with_attributes::calc);
std::vector<double> vec2 = benchmark(no_attributes::calc);
std::cout << "with attribute: " << std::fixed << std::setprecision(6) << calc_mean(vec1) << '\n';
std::cout << "without attribute: " << std::fixed << std::setprecision(6) << calc_mean(vec2) << '\n';
std::cout << "T statistics" << std::fixed << std::setprecision(6) << calc_ttest(vec1, vec2) << '\n';
}
根据 godbolt,这两个函数在 msvc
下生成相同的程序集 movsd xmm1, QWORD PTR __real@4000000000000000
comisd xmm0, xmm1
jbe SHORT $LN2@calc
xorps xmm1, xmm1
ucomisd xmm1, xmm0
ja SHORT $LN7@calc
sqrtsd xmm0, xmm0
ret 0
$LN7@calc:
jmp sqrt
$LN2@calc:
jmp pow
由于msvc不是开源编译器,只能猜测为什么msvc会选择忽略这个优化——可能因为两个分支都是函数调用(是尾调用所以jmp
而不是call
),这对 [[likely]] 来说太昂贵了,无法改变现状。
如果编译器改成clang,它会很聪明地把power 2优化成x * x,所以会生成不同的代码。按照这个思路,如果你的代码修改成
double calc(double x) noexcept {
if (x > 2)
return x + 1;
else
return x - 2;
}
msvc 也会输出不同的布局。
编译器很聪明。这些天,他们非常聪明。他们做了很多工作来弄清楚什么时候需要做事。
likely 和 unlikely 属性的存在是为了解决极其的具体问题。只有在对特定 performance-critical 代码的性能特征和生成的程序集进行深入分析后,问题才会变得明显。它们不是您在任何旧代码中涂抹以使其运行速度更快的药膏。
他们是手术刀。而且如果没有接受过手术训练,手术刀很可能会被误用。
因此,除非您对汇编分析显示可以通过更好的分支预测来解决的性能问题有特定的了解,否则您不应假设 任何 使用这些属性会使任何特定的代码都运行得更快。
也就是说,您得到的结果是完全合法的。