C ++ «Без сырых циклов» без потери производительности - PullRequest
1 голос
/ 23 октября 2019

Таким образом, «новая (старая) большая вещь» - это «Необработанные циклы» в C ++. Я пытаюсь написать код таким образом, но он кажется очень неэффективным. Да, есть алгоритмы STL, которые могут делать что угодно, но они не кажутся очень эффективными.

Например, у меня есть ситуация, когда я хочу указатель на узел в массиве узлов, который имеет самый высокийГол. Определение этого показателя является дорогостоящей операцией с плавающей запятой. Поэтому я реализовал версию алгоритма STL и сравнил ее с необработанным циклом:

#include <cfloat>
#include <iostream>
#include <array>
#include <algorithm>
#include <numeric>

static int counter;

class Node {
public:
    auto Score() const -> double {
        std::cout << "complex calculation\n";
        counter++;
        return 1;
    }
};

int main()
{

    std::array<Node, 10> nodes;

    counter = 0;
    Node const* nodePtr = std::max_element(std::cbegin(nodes), std::cend(nodes),
        [](Node const& node1, Node const& node2) {
            return node1.Score() < node2.Score();
        });
    std::cout << "algorithm count " << counter << std::endl;

    counter = 0;
    double maxScore = -FLT_MAX;
    for (const auto& node : nodes) {
        auto score = node.Score();
        if (score > maxScore) {
            maxScore = score;
            nodePtr = &node;
        }
    }
    std::cout << "raw loop count " << counter << std::endl;
}

Оценивая это, для версии STL дорогостоящая функция Score оценивается 18 раз, тогда как в необработанном цикле используется только 10 оценок. ..

Я делаю это неправильно или исходные циклы не так уж и плохи?

edit: После предположения user58697, что cout и статический счетчик помешают оптимизации компилятора, яизменил код:

#include <cfloat>
#include <cmath>
#include <iostream>
#include <array>
#include <algorithm>
#include <numeric>
#include <random>
#include <chrono>

template <typename T>
class Random {
private:
    std::default_random_engine generator;
    std::uniform_real_distribution<T> distribution;
public:
    Random()
        : generator()
        , distribution(0.0, 1.0)
    {}

    auto operator()() {
        return distribution(generator);
    };
};

static Random<double> myRandom;

class Timer {
private:
    std::chrono::high_resolution_clock::time_point startTime{};
public:
    void Start() noexcept {
        startTime = std::chrono::high_resolution_clock::now();
    }
    [[nodiscard]] auto ElapsedMs() const noexcept {
        return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - startTime).count();
    }
};

static Timer timer;

class Node {
private:
    double val;
public:
    Node() noexcept : val(myRandom()) {}

    [[nodiscard]] auto Score() const noexcept {
        auto score = std::sqrt(std::log(10.0 / val));
        score = std::sin(score) / std::cos(score);
        score = std::sqrt(std::sqrt(std::sqrt(std::sqrt(std::sqrt(score)))));
        score = std::pow(score, 1000);
        return score;
    }
};

int main()
{
    std::array<Node, 100000> nodes; // yeah, yeah... overloading the stack, I know

    for (auto i = 0; i < 2; i++) {
        timer.Start();
        Node const* nodePtr = &*std::max_element(std::cbegin(nodes), std::cend(nodes),
            [](Node const& node1, Node const& node2) {
                return node1.Score() < node2.Score();
            });
        std::cout << "algorithm elapsed time " << timer.ElapsedMs() << std::endl;

        timer.Start();
        double maxScore = -FLT_MAX;
        for (const auto& node : nodes) {
            auto score = node.Score();
            if (score > maxScore) {
                maxScore = score;
                nodePtr = &node;
            }
        }
        std::cout << "raw loop count " << timer.ElapsedMs() << std::endl;
    }
}

Я запускаю цикл дважды, чтобы исключить поведение при запуске ... результаты второго цикла (скомпилировано с g ++ 9.1 -O3):

algorithm elapsed time 16
raw loop count 8 (<== I see I forgot to change "count" to "time" :P)

Так что это не такэто.

Ответы [ 4 ]

2 голосов
/ 23 октября 2019

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

Однако вы сравниваете яблоки и апельсины здесь. Ваша реализация max_element всегда вычисляет Score() для ее сравнения, тогда как ваш цикл for кэширует результат функции Score().

Лучшая реализация Node может быть:

class Node {
mutable:
    double cached_score = std::numeric_limits<double>::quiet_Nan();
public:
    auto Score() const -> double {
        if(std::isnan(cached_score)){
           std::cout << "complex calculation\n";
           counter++;
           cached_score = 1;
        }
        return cached_score;
    }
    void invalidate_cache() {
      cached_score = std::numeric_limits<double>::quiet_Nan();
    }
};

Таким образом, сложное вычисление выполняется только один раз.

В качестве альтернативы напишите свою собственную абстракцию:

#include <cfloat>
#include <iostream>
#include <array>
#include <algorithm>
#include <numeric>

static int counter;

class Node {
public:
    auto Score() const -> double {
        std::cout << "complex calculation\n";
        counter++;
        return 1;
    }
};

template<class ForwardIt, class Evaluate, class Compare>
ForwardIt max_eval_element(
    ForwardIt first,
    ForwardIt last,
    Evaluate eval,
    Compare comp
){
    if (first == last) return last;

    ForwardIt largest = first;
    auto largest_val = eval(*first);
    ++first;
    for (; first != last; ++first) {
        const auto this_val = eval(*first);
        if (comp(largest_val, this_val)) {
            largest = first;
            largest_val = this_val;
        }
    }
    return largest;
}

int main()
{

    std::array<Node, 10> nodes;

    counter = 0;
    Node const* nodePtr = max_eval_element(std::cbegin(nodes), std::cend(nodes),
                                       [](Node const& node){ return node.Score(); },
                                       [](double const &a, double const &b) {
                                           return a<b;
                                       });
    std::cout << "algorithm count " << counter << std::endl;

    counter = 0;
    double maxScore = -FLT_MAX;
    for (const auto& node : nodes) {
        auto score = node.Score();
        if (score > maxScore) {
            maxScore = score;
            nodePtr = &node;
        }
    }
    std::cout << "raw loop count " << counter << std::endl;
}

В этом случае оба цикла выполняют одинаковое количество вычислений.

Многие внутренние кодовые базы, с которыми я работал, имеют обширные библиотеки, расширяющие STL. Это дает командам, над которыми я работал, большую уверенность в том, что их код написан правильно, и позволяет вам с первого взгляда интерпретировать сложные операции. Таким образом, эти абстракции также сокращают усилия по пониманию кода и общению.

1 голос
/ 24 октября 2019

Я просто хотел опубликовать алгоритм, который использовал, основываясь на ответе Ричарда:

template <typename FwdIt, typename Eval, typename Pred = std::less<>>
constexpr FwdIt max_eval_element(FwdIt first, FwdIt last, Eval eval, Pred pred = Pred()) {
    FwdIt found = first;
    if (first != last) {
        auto best = eval(*found);
        while (++first != last) {
            if (auto const thisVal = eval(*first);
                pred(best, thisVal)) {
                found = first;
                best = thisVal;
            }
        }
    }
    return found;
}

, что позволяет мне назвать алгоритм как

Node const* nodePtr = &*std::max_eval_element(std::cbegin(nodes), std::cend(nodes), std::mem_fn(&Node::Score));
1 голос
/ 23 октября 2019

Я думаю, что ваш случай несколько патологичен std::max_element, так как ваш компаратор каждый раз вызывает Score() для обоих элементов, и вы объявляете его дорогостоящим. Итак, где вы делаете:

for (const auto& node : nodes) {
    auto score = node.Score();
    if (score > maxScore) {
        maxScore = score;
        nodePtr = &node;
    }
}

... std::max_element эффективно должно сделать:

for (const auto& node : nodes) {
    if (node.Score() > nodePtr->Score()) {
        nodePtr = &node;
    }
}

Я думаю, std::max_element было бы просто замечательно в более нормальном случае, гдезначение для сравнения дешево для доступа.

0 голосов
/ 24 октября 2019

Кажется, проблема в том, как написан тест. Операторы

    std::cout << "complex calculation\n";
    count++;

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

Попробуйте использовать реальный Score (надеюсь, реальный - это чистых) и сравните время выполнения.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...