Почему мой код с вероятными/маловероятными атрибутами 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 также будет выводить другой макет.

Компиляторы умные. В наши дни они очень умны. Они много работают, чтобы понять, когда им нужно что-то сделать.

Вероятные и маловероятные атрибуты существуют для решения чрезвычайно специфических проблем. Проблемы, которые становятся очевидными только после глубокого анализа характеристик производительности и сгенерированной сборки конкретного фрагмента кода, критичного для производительности. Это не мазь, которую вы втираете в любой старый код, чтобы он работал быстрее.

Они скальпель. А без хирургической подготовки скальпелем, скорее всего, злоупотребят.

Поэтому, если у вас нет конкретных знаний о проблеме производительности, которую, как показывает анализ сборки, можно решить путем лучшего прогнозирования ветвлений, вы не должны предполагать, что любое использование этих атрибутов ускорит работу любого конкретного кода.

То есть результат, который вы получаете, вполне законен.

Другие вопросы по тегам