Pull to refresh

Медленный std::count_if в Visual Studio 2013

После прочтения статьи решил проверить как справляются с оптимизацией различные компиляторы на примере алгоритма STL std::count_if. Для этого был написан следующий код:

#include <iostream>
#include <vector>
#include <algorithm>
#include <chrono>
#include <random>

int
main(int argc, char **argv)
{
    std::mt19937 gen;
    std::uniform_int_distribution<int> dist(0, 10);
    std::vector<int> arr(1000000);
    for (auto& item : arr) {
        item = dist(gen);
    }
    
    size_t total_time = 0;
    size_t count_times = 0;
    for (decltype(arr)::value_type num = 0; num <= 10; ++num) {
        const auto start_time = std::chrono::high_resolution_clock::now();
        count_times = 0;
        for (size_t iter = 0; iter < 100; ++iter) {
            count_times += std::count_if(std::begin(arr), std::end(arr), [&num](int elem){ return (elem < num);});
        }
        const auto end_time = std::chrono::high_resolution_clock::now();
        const auto t = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time).count();
        std::cout << num << " - " << t << "\n";
        total_time += t;
    }
    std::cout << "total_time  = " << total_time << "\n";
    std::cout << "count_times = " << count_times << "\n";

    return 0;
}


Вначале мы заполняем вектор arr случайными числами с равномерным распределением, а затем подсчитываем количество элементов в векторе, которые меньше определенного значения (num) и выводим время, затраченное на подсчет. Вывод переменной count_times в конце программы нужен для того, чтобы компилятор не выкинул цикл

for (size_t iter = 0; iter < 100; ++iter) {
    count_times += std::count_if(std::begin(arr), std::end(arr), [&num](int elem){ return (elem < num);});
}
после оптимизации. Версия компилятора, ключи компиляции и результаты работы можно увидеть на следующем скриншоте:



Т.е. время, затраченное на подсчет элементов, значение которых меньше 0 — это время работы цикла, без выполнения команды инкремента (поскольку у нас в векторе нет элементов, меньших 0). Также можно заметить, что время, затраченное на подсчет элементов, значение которых меньше 5 в 6 раз превосходит время работы цикла, без выполнения команды инкремента. Неужели инкремент занимает столько много времени? Для этого данный код был откомпилирован на g++ (вресия компилятор, ключи компиляции и результаты работы представлены на скриншоте):



Т.е. видно, что g++ справился с задачей на отлично: время выполнения практически в 3 раза быстрее, чем у cl.exe. Если посмотреть на дизассемблерный фрагмент кода:
count_times += std::count_if(std::begin(arr), std::end(arr), [&num](int elem){ return (elem < num);});
, то станет ясно, что причиной всему — предсказатель переходов (branch predictor). Вот ассемблерный код от компилятора cl.exe:



А вот как с этим же заданием справился g++:



Видно, что cl.exe использовал условный переход, в то время как g++ использовал ассемблерную команду cmovl (conditional move if less), поэтому программа, откомпилированная с помощью g++ работает быстрее, поскольку процессору не пришлось предсказывать переход и очищать кеш-линии в случае неудачно предсказанного перехода.
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.