Как избежать оператора if внутри цикла for?

116

У меня есть класс Writerс такой функцией writeVector:

void Drawer::writeVector(vector<T> vec, bool index=true)
{
    for (unsigned int i = 0; i < vec.size(); i++) {
        if (index) {
            cout << i << "\t";
        }
        cout << vec[i] << "\n";
    }
}

Я стараюсь не дублировать код, но при этом беспокоюсь о производительности. В этой функции я if (index)проверяю каждый раунд своего forцикла, хотя результат всегда один и тот же. Это против того, чтобы «беспокоиться о производительности».

Я мог бы легко избежать этого, разместив проверку за пределами моего forцикла. Однако я получу кучу повторяющегося кода:

void Drawer::writeVector(...)
{
    if (index) {
        for (...) {
            cout << i << "\t" << vec[i] << "\n";
        }
    }
    else {
        for (...) {
            cout << vec[i] << "\n";
        }
    }
}

Так что для меня это оба «плохих» решения. Я думал о двух частных функциях, одна из которых выходит за пределы индекса, а затем вызывает другую. Другой только превосходит ценность. Однако я не могу понять, как использовать его с моей программой, мне все равно нужна ifпроверка, чтобы узнать, какую из них вызывать ...

Согласно проблеме, полиморфизм кажется правильным решением. Но я не понимаю, как мне его здесь использовать. Каким будет предпочтительный способ решения такой проблемы?

Это не настоящая программа, мне просто интересно узнать, как следует решать такого рода проблемы.

Скама Один
источник
8
@JonathonReinhart Может быть, некоторые люди хотят изучать программирование и им интересно, как решать проблемы?
Skamah One 01
9
Я дал этому вопросу +1. Такая оптимизация может не потребоваться часто, но, во-первых, указание на этот факт может быть частью ответа, а во-вторых, редкие типы оптимизации по-прежнему очень актуальны для программирования.
jogojapan 01
31
Речь идет о хорошем дизайне, который позволяет избежать дублирования кода и сложной логики внутри цикла. Это хороший вопрос, не надо его отрицать.
Али
5
Это интересный вопрос, обычно проходы преобразования цикла в компиляторе решают его очень эффективно. если функция достаточно мала, как эта, инлайнер позаботится об этом и, скорее всего, полностью уничтожит ветвь. Я бы предпочел изменить код до тех пор, пока инлайнер не будет успешно встраивать код, чем решать эту проблему с помощью шаблонов.
Alex
5
@JonathonReinhart: А? Первая редакция вопроса практически идентична этой. Ваше "почему вас это волнует?" комментарий на 100% не имеет отношения ко всем исправлениям. Что же касается публичных выговоров - это не только вы, а здесь много людей, которые создают эту проблему. Когда заголовок «избегает операторов if внутри цикла for» , должно быть довольно очевидно, что вопрос общий, а пример приведен только для иллюстрации . Вы никому не помогаете, когда игнорируете вопрос и выставляете ОП глупым из-за конкретного иллюстративного примера, который он использовал.
user541686 01

Ответы:

79

Передайте тело цикла как функтор. Он встроен во время компиляции, без потери производительности.

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

Если вам разрешено использовать C ++ 11, вы можете сделать что-то вроде этого:

#include <iostream>
#include <set>
#include <vector>

template <typename Container, typename Functor, typename Index = std::size_t>
void for_each_indexed(const Container& c, Functor f, Index index = 0) {

    for (const auto& e : c)
        f(index++, e);
}

int main() {

    using namespace std;

    set<char> s{'b', 'a', 'c'};

    // indices starting at 1 instead of 0
    for_each_indexed(s, [](size_t i, char e) { cout<<i<<'\t'<<e<<'\n'; }, 1u);

    cout << "-----" << endl;

    vector<int> v{77, 88, 99};

    // without index
    for_each_indexed(v, [](size_t , int e) { cout<<e<<'\n'; });
}

Этот код не идеален, но идею вы поняли.

В старом C ++ 98 это выглядело так:

#include <iostream>
#include <vector>
using namespace std;

struct with_index {
  void operator()(ostream& out, vector<int>::size_type i, int e) {
    out << i << '\t' << e << '\n';
  }
};

struct without_index {
  void operator()(ostream& out, vector<int>::size_type i, int e) {
    out << e << '\n';
  }
};


template <typename Func>
void writeVector(const vector<int>& v, Func f) {
  for (vector<int>::size_type i=0; i<v.size(); ++i) {
    f(cout, i, v[i]);
  }
}

int main() {

  vector<int> v;
  v.push_back(77);
  v.push_back(88);
  v.push_back(99);

  writeVector(v, with_index());

  cout << "-----" << endl;

  writeVector(v, without_index());

  return 0;
}

Опять же, код далек от совершенства, но он дает вам идею.

Али
источник
4
for(int i=0;i<100;i++){cout<<"Thank you!"<<endl;}: D Это то решение, которое я искал, оно работает как шарм :) Вы могли бы улучшить его с помощью нескольких комментариев (сначала были проблемы с его пониманием), но у меня это получилось, поэтому нет проблем :)
Skamah One
1
Я рад, что это помогло! Пожалуйста, проверьте мое обновление с кодом C ++ 11, он менее раздут по сравнению с версией C ++ 98.
Али
3
Nitpick: это нормально в примере с OP, потому что тело цикла настолько мало, но если бы он был больше (представьте себе дюжину строк кода вместо одной cout << e << "\n";), все равно было бы некоторое дублирование кода.
syam 01
3
Почему в примере C ++ 03 используются структуры и перегрузка операторов? Почему бы просто не сделать две функции и не передать им указатели?
Malcolm
2
@Malcolm Inlining. Если они являются структурами, скорее всего, вызовы функций могут быть встроены. Если вы передадите указатель на функцию, скорее всего, эти вызовы не могут быть встроены.
Али
40

В этой функции я выполняю проверку if (index) на каждом этапе цикла for, даже если результат всегда один и тот же. Это против того, чтобы «беспокоиться о производительности».

Если это действительно так, у предсказателя ветвления не будет проблем с предсказанием (постоянного) результата. Таким образом, это приведет лишь к незначительным накладным расходам из-за неверных прогнозов на первых нескольких итерациях. Не о чем беспокоиться с точки зрения производительности

В этом случае я рекомендую держать тест внутри цикла для ясности.

Марк Клезен
источник
3
Это просто пример, я здесь, чтобы узнать, как следует решить эту проблему. Мне просто любопытно, даже не создаю настоящей программы. Надо было упомянуть об этом в вопросе.
Skamah One 01
40
В таком случае имейте в виду, что преждевременная оптимизация - это корень всех зол . При программировании всегда сосредотачивайтесь на удобочитаемости кода и убедитесь, что другие понимают, что вы пытаетесь сделать. Рассматривайте микрооптимизации и различные взломы только после профилирования вашей программы и выявления горячих точек . Вы никогда не должны рассматривать оптимизацию, не установив в ней необходимости. Очень часто проблемы с производительностью возникают не там, где вы ожидаете.
Marc Claesen 01
3
И в этом конкретном примере (хорошо, понятно, это всего лишь пример) очень вероятно, что время, потраченное на управление циклом и тест, почти незаметно рядом со временем, затраченным на ввод-вывод. Это часто проблема C ++: выбор между удобочитаемостью за счет обслуживания и (гипотетической) эффективностью.
Крисс
8
Вы предполагаете, что код выполняется на процессоре, который изначально имеет предсказание ветвления. Большинство систем, работающих на C ++, этого не делают. (Хотя, наверное, у большинства систем есть полезная std::coutфункция)
Бен Фойгт
2
-1. Да, здесь хорошо работает предсказание ветвлений. Да, условие может быть действительно поднято компилятором вне цикла. Да, ПУЙТРОАЭ. Но ветвления внутри цикла - это опасная вещь, которая часто влияет на производительность, и я не думаю, что игнорировать их, просто сказав «предсказание ветвлений», - это хороший совет, если кто-то действительно заботится о производительности. Наиболее примечательным примером является то, что векторизирующий компилятор будет нуждаться в предсказании для обработки этого, производя менее эффективный код, чем для циклов без ветвей.
Oak
35

Чтобы расширить ответ Али, который совершенно правильный, но все же дублирует некоторый код (часть тела цикла, этого, к сожалению, вряд ли можно избежать при использовании шаблона стратегии) ​​...

Допустим, в этом конкретном случае дублирование кода невелико, но есть способ еще больше уменьшить его, что пригодится, если тело функции больше, чем несколько инструкций .

Ключ в том, чтобы использовать способность компилятора выполнять постоянное сворачивание / устранение мертвого кода . Мы можем сделать это, вручную сопоставив значение времени выполнения со значением времени indexкомпиляции (это легко сделать, когда есть только ограниченное количество случаев - в данном случае два) и использовать аргумент шаблона без типа, который известен при компиляции -время:

template<bool index = true>
//                  ^^^^^^ note: the default value is now part of the template version
//                         see below to understand why
void writeVector(const vector<int>& vec) {
    for (size_t i = 0; i < vec.size(); ++i) {
        if (index) { // compile-time constant: this test will always be eliminated
            cout << i << "\t"; // this will only be kept if "index" is true
        }
        cout << vec[i] << "\n";
    }
}

void writeVector(const vector<int>& vec, bool index)
//                                            ^^^^^ note: no more default value, otherwise
//                                            it would clash with the template overload
{
    if (index) // runtime decision
        writeVector<true>(vec);
        //          ^^^^ map it to a compile-time constant
    else
        writeVector<false>(vec);
}

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

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

writeVector<true>(vec);   // you already know at compile-time which version you want
                          // no need to go through the non-template runtime dispatching

writeVector(vec, index);  // you don't know at compile-time what "index" will be
                          // so you have to use the non-template runtime dispatching

writeVector(vec);         // you can even use your previous syntax using a default argument
                          // it will call the template overload directly
сям
источник
2
Помните, что вы удалили дублирование кода за счет усложнения логики внутри цикла. Я не вижу ни лучше, ни хуже, чем то, что я предложил для этого конкретного простого примера. +1 в любом случае!
Али
1
Мне нравится ваше предложение, потому что оно показывает еще одну возможную оптимизацию. Вполне возможно, что index с самого начала может быть константой шаблона. В этом случае он может быть заменен константой времени выполнения вызывающим элементом writeVector, а writeVector изменен на некоторый шаблон. Избегайте дальнейшего изменения исходного кода.
Крисс
1
@kriss: На самом деле мое предыдущее решение уже позволяло это, если вы звонили doWriteVectorнапрямую, но я согласен, что имя было неудачным. Я просто изменил его, чтобы иметь две перегруженные writeVectorфункции (одну шаблонную, другую - обычную), чтобы результат был более однородным. Спасибо за предложение. ;)
syam 01
4
ИМО, это лучший ответ. +1
user541686 01
1
@Mehrdad За исключением того, что он не отвечает на исходный вопрос. Как избежать оператора if внутри цикла for? Тем не менее, он отвечает, как избежать потери производительности. Что касается «дублирования», потребуется более реалистичный пример с вариантами использования, чтобы увидеть, как его лучше всего исключить. Как я уже сказал, я поддержал этот ответ.
Али
0

В большинстве случаев ваш код уже хорош по производительности и удобочитаемости. Хороший компилятор способен обнаруживать инварианты цикла и выполнять соответствующие оптимизации. Рассмотрим следующий пример, очень близкий к вашему коду:

#include <cstdio>
#include <iterator>

void write_vector(int* begin, int* end, bool print_index = false) {
    unsigned index = 0;
    for(int* it = begin; it != end; ++it) {
        if (print_index) {
            std::printf("%d: %d\n", index, *it);
        } else {
            std::printf("%d\n", *it);
        }
        ++index;
    }
}

int my_vector[] = {
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
};


int main(int argc, char** argv) {
    write_vector(std::begin(my_vector), std::end(my_vector));
}

Я использую следующую командную строку для его компиляции:

g++ --version
g++ (GCC) 4.9.1
Copyright (C) 2014 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
g++ -O3 -std=c++11 main.cpp

Затем дамп сборки:

objdump -d a.out | c++filt > main.s

Результат сборки write_vector:

00000000004005c0 <write_vector(int*, int*, bool)>:
  4005c0:   48 39 f7                cmp    %rsi,%rdi
  4005c3:   41 54                   push   %r12
  4005c5:   49 89 f4                mov    %rsi,%r12
  4005c8:   55                      push   %rbp
  4005c9:   53                      push   %rbx
  4005ca:   48 89 fb                mov    %rdi,%rbx
  4005cd:   74 25                   je     4005f4 <write_vector(int*, int*, bool)+0x34>
  4005cf:   84 d2                   test   %dl,%dl
  4005d1:   74 2d                   je     400600 <write_vector(int*, int*, bool)+0x40>
  4005d3:   31 ed                   xor    %ebp,%ebp
  4005d5:   0f 1f 00                nopl   (%rax)
  4005d8:   8b 13                   mov    (%rbx),%edx
  4005da:   89 ee                   mov    %ebp,%esi
  4005dc:   31 c0                   xor    %eax,%eax
  4005de:   bf a4 06 40 00          mov    $0x4006a4,%edi
  4005e3:   48 83 c3 04             add    $0x4,%rbx
  4005e7:   83 c5 01                add    $0x1,%ebp
  4005ea:   e8 81 fe ff ff          callq  400470 <printf@plt>
  4005ef:   49 39 dc                cmp    %rbx,%r12
  4005f2:   75 e4                   jne    4005d8 <write_vector(int*, int*, bool)+0x18>
  4005f4:   5b                      pop    %rbx
  4005f5:   5d                      pop    %rbp
  4005f6:   41 5c                   pop    %r12
  4005f8:   c3                      retq   
  4005f9:   0f 1f 80 00 00 00 00    nopl   0x0(%rax)
  400600:   8b 33                   mov    (%rbx),%esi
  400602:   31 c0                   xor    %eax,%eax
  400604:   bf a8 06 40 00          mov    $0x4006a8,%edi
  400609:   48 83 c3 04             add    $0x4,%rbx
  40060d:   e8 5e fe ff ff          callq  400470 <printf@plt>
  400612:   49 39 dc                cmp    %rbx,%r12
  400615:   75 e9                   jne    400600 <write_vector(int*, int*, bool)+0x40>
  400617:   eb db                   jmp    4005f4 <write_vector(int*, int*, bool)+0x34>
  400619:   0f 1f 80 00 00 00 00    nopl   0x0(%rax)

Мы видим, что в начале функции мы проверяем значение и переходим к одному из двух возможных циклов:

  4005cf:   84 d2                   test   %dl,%dl
  4005d1:   74 2d                   je     400600 <write_vector(int*, int*, bool)+0x40>

Конечно, это работает только в том случае, если компилятор способен определить, что условие фактически инвариантно. Обычно он отлично работает с флагами и простыми встроенными функциями. Но если состояние «сложное», рассмотрите возможность использования подходов из других ответов.

ivaigult
источник