Веселые старты 2: for_each vs accumulate

от автора


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

Наша цель — понять, насколько можно доверять оптимизацию своего кода компилятору, и можно ли облегчить (осложнить) его задачу?

Почему все одинаково?

Повторим наши замеры (смотри предыдущую статью), попросив компилятор оптимизировать наш код. Чтобы компилятор не увлекся, в код добавим вывод суммы:

cout << "sum=" << sum << endl; 

Компилируем вариант с accumulate:

sum = accumulate(vec.begin(), vec.end(), 0); 

Весь код

#include <iostream> #include <sys/time.h> #include <iomanip> #include <vector> #include <algorithm> #include <functional>  using namespace std; typedef int A; const int COUNT = 1000 * 1000 * 1000;  int main () {     vector<A>vec(COUNT);     generate(begin(vec), end(vec), std::rand);     A sum = 0;     struct timespec tm1, tm2;      clock_gettime(CLOCK_REALTIME, &tm1);     sum = accumulate(vec.begin(), vec.end(), A(0));     clock_gettime(CLOCK_REALTIME, &tm2);      cout << "accumulate" << endl;     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;     cout << "sum=" << sum << endl;      return 0; }; 

с максимальной оптимизацией:

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

Сравним этот результат с результатом for_each:

for_each(vec.begin(), vec.end(), [&sum](int i) {     sum += i; }); 

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

Варианты с явным циклом дают схожий результат.
Почему скорость после оптимизации стала одинаковой? Для того, чтобы ответить на этот вопрос, давайте заглянем в STL и посмотрим, что из себя представляет функция for_each:

template<typename _InputIterator, typename _Function> for_each(_InputIterator __first, _InputIterator __last, _Function __f) {     for (; __first != __last; ++__first)         __f(*__first);     return__f; } 

Как видно for_each это и есть цикл, единственная оптимизация — компилятор делает функцию for_each встроенной:

for (; __first != __last; ++__first)     [&sum](int i) {         sum += i;     }(*__first); 

Тут компилятору кажется разумным и лямбду сделать встроенной. Итераторы вектора по своей сути — указатели, поэтому в итоге конечный ассемблерный код выглядит приблизительно так:

.L4:        addq    $1, %rax        paddd   (%rcx), %xmm0        addq    $16, %rcx        cmpq    %r9, %rax        jb      .L4 

Даже если бы я писал код вручную, у меня не получилось бы лучше. Так как программа у нас небольшая и переменных совсем немного, компилятор поместил их в регистры. Никакого лишнего копирования или вызовов функций — превосходно, даже сложение происходит сразу по двум элементам.
Теперь становиться очевидным, почему на скорость конечного кода не влияют “ручные” оптимизации циклов, и даже ключевое слово register.

Всегда ли скорость одинаковая?

Давайте вместо int суммировать простенький классик размером с sizeof(int):

class A {    int v = 0; public:    A() {}    A(int i);    A(const A& a);    operator int();    A operator +=(const A& a);    A operator +(const A& a); }; 

А его реализацию поместим в отдельный файл.

a.cpp

A::A(int i) : v(i) {} A::A(const A &a) : v(a.v) {} A::operator int() {      return v;  } A A::operator +=(const A &a){    v += a.v;    return v; } A A::operator +(const A &a) {      return a.v + v; } 

Теперь вариант с for_each:

for_each(vec.begin(), vec.end(), [&](A i) {    sum += i; }); 

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

$ g++ -std=c++11 main.cpp a.cpp -o test -O3 && ./test  $ duration 372.84 ms mseconds 

И простой цикл:

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

Запускаем:

$ g++ -std=c++11 main.cpp a.cpp -o test -O3 && ./test  $ duration 240.57 ms mseconds 

Неужели циклы все-таки быстрее? На самом деле — нет, просто корректный вариант с for_each теперь должен выглядеть так:

for_each(vec.begin(), vec.end(), [&](A &i) {    sum += i; }); 

Тогда:

$ g++ -std=c++11 main.cpp a.cpp -o test -O3 && ./test  $ duration 240.8 ms mseconds 

Дело в том, что компилятор не имеет право просто убрать копирование аргумента, так как он не знает, какие добрые дела мы творим в написанном нами операторе копирования.
Скорость работы for_each сравнялась со скоростью цикла, а вот вариант с accumulate

sum = accumulate(vec.begin(), vec.end(), A(0)); 

все-таки отстает:

$ g++ -std=c++11 main.cpp a.cpp -o test -O3 && ./test  $ duration 410.52 ms mseconds 

Почему так? Посмотрим на ассемблерный код варианта с for_each:

.L12:        leaq    160(%rsp), %rsi        leaq    112(%rsp), %rdi        movq    %rbx, %rdx        call    _ZN1ApLERKS_        addq    $4, %rbx        cmpq    %rbp, %rbx        jne     .L12 

И сравним его с кодом варианта с accumulate:

.L7:        leaq    144(%rsp), %rsi        leaq    192(%rsp), %rdi        movq    %rbx, %rdx        call    _ZN1AplERKS_        movl    192(%rsp), %eax        addq    $4, %rbx        cmpq    %rbx, %rbp        movl    %eax, 144(%rsp)        jne     .L7 

Все из-за того, что компилятор из оператора "+=" генерирует более легкий код, чем из присваивания суммы:

template<typename _InputIterator, typename _Tp> accumulate(_InputIterator __first, _InputIterator __last, _Tp __init) {    for (; __first != __last; ++__first)        __init = __init + *__first;    return __init; } 

Отсюда и лишние операции перемещения.
Кстати, вам не кажется странным тот факт, что специализированная функция для подсчета суммы дает худший результат, чем цикл или использование for_each?

Вывод

Компилятор не на столько умен, как человек. И достаточно легкого изменения кода, чтобы компилятор запутался и выдал нам не совсем то, что мы бы хотели или ожидали.
Если мы хотим получить гарантированный результат — нужно все делать “самому”, либо каждый раз проверять, что там компилятор улучшил, а что — ухудшил. Стандартом не гарантируется, что for_each будет оптимизирована так же хорошо, как и цикл, а это важно, если вы пишете переносимый код.
Если же скорость работы не критична — всегда выбирайте STL. Код получается более читабельным, и в среднем код на STL работает быстрее.

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


Комментарии

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

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