Почему мой код с вероятными/маловероятными атрибутами C++20 не работает быстрее?
Код выполнялся в Visual Studio 2019 версии 16.11.8 с оптимизацией /O2 и процессором Intel. Я пытаюсь найти основную причину этого нелогичного результата. Я получаю, что без атрибутов статистически быстрее, чем с атрибутами через 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';
}
2 ответа
Согласно 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
), и это слишком дорого, чтобы [[вероятно]] что-то изменить.
Если компилятор изменен на clang, он достаточно умен, чтобы оптимизировать мощность 2 до x * x, поэтому будет сгенерирован другой код. Следуя этому примеру, если ваш код изменен на
double calc(double x) noexcept {
if (x > 2)
return x + 1;
else
return x - 2;
}
msvc также будет выводить другой макет.
Компиляторы умные. В наши дни они очень умны. Они много работают, чтобы понять, когда им нужно что-то сделать.
Вероятные и маловероятные атрибуты существуют для решения чрезвычайно специфических проблем. Проблемы, которые становятся очевидными только после глубокого анализа характеристик производительности и сгенерированной сборки конкретного фрагмента кода, критичного для производительности. Это не мазь, которую вы втираете в любой старый код, чтобы он работал быстрее.
Они скальпель. А без хирургической подготовки скальпелем, скорее всего, злоупотребят.
Поэтому, если у вас нет конкретных знаний о проблеме производительности, которую, как показывает анализ сборки, можно решить путем лучшего прогнозирования ветвлений, вы не должны предполагать, что любое использование этих атрибутов ускорит работу любого конкретного кода.
То есть результат, который вы получаете, вполне законен.