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

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

Т.е. видно, что g++ справился с задачей на отлично: время выполнения практически в 3 раза быстрее, чем у cl.exe. Если посмотреть на дизассемблерный фрагмент кода:

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

Видно, что cl.exe использовал условный переход, в то время как g++ использовал ассемблерную команду cmovl (conditional move if less), поэтому программа, откомпилированная с помощью g++ работает быстрее, поскольку процессору не пришлось предсказывать переход и очищать кеш-линии в случае неудачно предсказанного перехода.
#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++ работает быстрее, поскольку процессору не пришлось предсказывать переход и очищать кеш-линии в случае неудачно предсказанного перехода.