for 루프 내부에서 if 문을 피합니까?

다음 과 같은 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어떤 전화를 걸지 확인하려면 여전히 확인이 필요합니다 …

문제에 따르면 다형성은 올바른 해결책처럼 보입니다. 하지만 여기서 어떻게 사용해야할지 모르겠습니다. 이런 종류의 문제를 해결하는 데 선호되는 방법은 무엇입니까?

이것은 실제 프로그램이 아닙니다. 저는 이런 종류의 문제를 어떻게 해결해야하는지 배우는 데 관심이 있습니다.



답변

펑터로 루프의 본문을 전달하십시오. 성능 저하없이 컴파일 타임에 인라인됩니다.

다양한 것을 전달한다는 아이디어는 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;
}

다시 말하지만 코드는 완벽하지는 않지만 아이디어를 제공합니다.


답변

함수에서 결과가 항상 동일하더라도 for 루프의 모든 라운드에서 if (인덱스) 검사를 수행하고 있습니다. 이것은 “성능에 대한 걱정”에 위배됩니다.

이것이 사실이라면 분기 예측기는 (일정한) 결과를 예측하는 데 문제가 없습니다. 따라서 처음 몇 번의 반복에서 잘못된 예측에 대한 경미한 오버 헤드 만 발생합니다. 성능 측면에서 걱정할 필요가 없습니다.

이 경우 명확성을 위해 루프 내부에 테스트를 유지하는 것을 옹호합니다.


답변

Ali의 대답을 확장하려면 완벽하게 정확하지만 여전히 일부 코드를 복제합니다 (루프 본문의 일부, 불행히도 전략 패턴을 사용할 때 거의 피할 수 없습니다) …

이 특별한 경우에 허용되는 코드 중복은 많지 않지만 더 많이 줄일 수있는 방법이 있습니다. 이는 함수 본문이 몇 개의 명령어보다 큰 경우 유용 합니다 .

핵심은 컴파일러의 기능을 사용하여 지속적인 폴딩 / 데드 코드 제거 를 수행하는 것 입니다. 런타임 값을 index컴파일 시간 값 에 수동으로 매핑하고 (제한된 수의 경우-이 경우 2 개만있을 때 쉽게 수행 할 수 있음) 컴파일시 알려진 비 형식 템플릿 인수를 사용하여이를 수행 할 수 있습니다. -시각:

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);
}

이렇게하면 두 번째 코드 예제 (outer if/ inner 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


답변

대부분의 경우 코드는 이미 성능과 가독성에 좋습니다. 좋은 컴파일러는 루프 불변을 감지하고 적절한 최적화를 수행 할 수 있습니다. 코드와 매우 가까운 다음 예제를 고려하십시오.

#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>

물론 이것은 컴파일러가 조건이 실제 불변임을 감지 할 수있는 경우에만 작동합니다. 일반적으로 플래그 및 간단한 인라인 함수에 대해 완벽하게 작동합니다. 그러나 조건이 “복잡”한 경우 다른 답변의 접근 방식을 사용하는 것이 좋습니다.


답변