Веселые старты или C++ и STL: кто быстрее?

от автора


Постановка задачи

Нас интересует скорость различных стандартных инструментов C++ для выполнения однотипных операций над большим количеством элементов (циклы, алгоритмы STL, итераторы, указатели и т.д.). Для упрощения будем считать исходной задачу вычисления суммы большого количества целых чисел. (Ссылка для тех, кто не любит читать, а любит смотреть.)

Prepare

Для начала напишем код, который будет оценивать быстродействие цикла:

#include <iostream> #include <sys/time.h> #include <iomanip>  using namespace std;  const int COUNT = 1000 * 1000 * 1000;  int main () {        struct timespec tm1, tm2;        clock_gettime(CLOCK_REALTIME, &tm1);   // Our code to estimate         clock_gettime(CLOCK_REALTIME, &tm2);         double t1 = 1000.0 * tm1.tv_sec + tm1.tv_nsec / (1000.0 * 1000);        double t2 = 1000.0 * tm2.tv_sec + tm2.tv_nsec / (1000.0 * 1000);        cout << "t=" << setprecision(5) << t2 -t1 << " ms" << endl;        return 0; };  

Не очень точно, но зато просто и понятно.

При последовательном переборе элементов в STL самым быстрым контейнером является вектор, так как его элементы располагаются последовательно. Так что создадим вектор размером COUNT и заполним его (почти) случайными целыми значениями:

 #include <vector> #include <algorithm> … int RandomNumber () {        static int s = 0;        return (s++ % 100); } int main () {        vector<int> vec(COUNT);        generate(vec.begin(), vec.end(), RandomNumber);        ...        // Our code to estimate  

Accumulate

Самым кошерным способом решения нашей задачи является использование функции accumulate:

#include <numeric>        …        // Our code to estimate        accumulate(vec.begin(), vec.end(), 0, [](int sum, int i) {                return sum + i;        });  

Компилируем и запускаем:

$ g++ -std=c++11 main.cpp -o test && ./test  $ duration 349.29 mseconds 

Не мало…

for_each

Попробуем for_each?

// Our code to estimate int sum = 0; for_each(vec.begin(), vec.end(), [&sum](int i) {        sum += i; }); 

Смотрим:

$ g++ -std=c++11 main.cpp -o test && ./test  $ duration 417.64 mseconds 

Интересно, что если использовать объект с состоянием, получится чуть быстрее: передача по ссылке в лямбду чуток нас тормозит:

class Sum {        int sum; public:        Sum() : sum(0) {}        inline void operator()(int i) {                sum += i;        }        inline operator int() {                return sum;        } }; …        // Our code to estimate        for_each(vec.begin(), vec.end(), Sum());  

Барабанная дробь…

$ g++ -std=c++11 main.cpp -o test && ./test  $ duration 371.64 mseconds 

Еще можно сделать лямбду с состоянием и получить результат, сравнимый с accumulate, но игра не стоит свеч: результат все равно нас не устроит.

for

Вернемся к старым добрым циклам:

// Our code to estimate int sum = 0; for(auto i = vec.begin(); i != vec.end(); i++) {        sum += *i; }; 

Ну и что?

$ g++ -std=c++11 main.cpp -o test && ./test  $ duration 520.66 mseconds 

Ну, это понятно, почему, меняем немного код:

auto end = vec.end(); for(auto i = vec.begin(); i != end; i++) { 

Снова компилируем и запускаем:

$ g++ -std=c++11 main.cpp -o test && ./test  $ duration 403.68 mseconds 

Уже лучше! Еще мы знаем, что префиксный инкремент чуть быстрее чем постфиксный:

for(auto i = vec.begin(); i != end; ++i) { 

Это потому, что в операции постфиксного инткремента используется временный объект + копирование.

Компилируем и запускаем:

$ g++ -std=c++11 main.cpp -o test && ./test  $ duration 327.05 mseconds 

Лучший результат на сегодня! Но еще не вечер…
Почему цикл оказался чуть быстрее? В основном потому, что тут нет вызова функций, а также нет лишних копирований объекта (в нашем случае — целочисленного) суммы.

iterator or not

Откажемся от итераторов вообще:

for(int i = 0; i < COUNT; ++i) {    sum += vec[i]; }; 

Компилируем и запускаем:

$ g++ -std=c++11 main.cpp -o test && ./test  $ duration 110.52 mseconds 

Вот это уже — результат.

Тут мы выиграли сразу на двух операциях — инкремент инта быстрее, чем оператор инкремента у итератора, а оператор разыменовывания медленнее, чем доступ по индексу.

Arrays

А что будет, если мы используем обычный массив? Точнее, будем брать элементы по смещению указателя, а не по индексу (замена вектора на массив бессмыслена — вектор хранит свои данные в массиве):

int *array = &vec[0]; for(int i = 0; i < COUNT; ++i) {        sum += array[i]; }; 

И оно того стоило:

$ g++ -std=c++11 main.cpp -o test && ./test  $ duration 81.219 mseconds 

Доступ по указателю намного быстрее!
А если итерировать сразу по указателю, без индексов, выйдет значительно быстрее:

auto end = &vec[vec.size () - 1]; for(int *i = &vec[0]; i != end; ++i) {    sum += *i; }; 

$ g++ -std=c++11 main.cpp -o test && ./test  $ duration 24.494 mseconds 

Хардкор

Что еще мы можем сделать с циклом? Точно — «зарегистрировать»:

register int *array = &vec[0]; register int sum = 0; for(register int i = 0; i != COUNT; ++i) {        sum += array[i]; }; 

И что мы получаем?

$ g++ -std=c++11 main.cpp -o test && ./test  $ duration 10.456 mseconds 

Результат радует, однако следует помнить, что злоупотребление модификатором register сводит его на нет: компилятор перестает обращать на него внимание когда регистров не хватает, а их всегда не хватает) Так что вышеприведенный результат представляет скорее академический интерес, чем практический.

Можно по-эксперементировать с алгоритмами, например, такое изменение:

sum += array[i] + array[++i]; 

Ускорит наш код еще в двое:

$ g++ -std=c++11 main.cpp -o test && ./test  $ duration 4.8562 mseconds 

Но это — тема отдельной статьи… (За рамки статьи также вышли оптимизации компилятора, мы их не используем.)

Вывод

Скорость работы нашего цикла увеличилась на порядок! При этом мы даже не думали о многопоточных алгоритмах (и алгоритмах вообще). Мы также не использовали ничего, кроме родного C++: действовали строго по стандарту и не использовали никаких сторонних библиотек.

image

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

ссылка на оригинал статьи http://habrahabr.ru/post/260025/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *