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


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

9
나는이 질문에 +1을 주었다. 이러한 종류의 최적화는 종종 필요하지 않을 수 있지만 첫째,이 사실이 해답의 일부가 될 수 있다는 점을 지적하고, 둘째, 드문 유형의 최적화는 여전히 프로그래밍과 관련성이 높습니다.
jogojapan

31
문제는 루프 내부의 코드 중복과 복잡한 논리를 피하는 좋은 디자인에 관한 것입니다. 좋은 질문입니다. 찬성 할 필요가 없습니다.
Ali

5
흥미로운 질문입니다. 일반적으로 컴파일러의 루프 변환 패스가이를 매우 효율적으로 해결합니다. 함수가 이와 같이 충분히 작 으면 인라이너가이를 처리하고 분기를 완전히 제거 할 가능성이 높습니다. 템플릿으로이 문제를 해결하는 것보다 인라이너가 코드를 행복하게 인라인 할 때까지 코드를 변경하고 싶습니다.
Alex

5
@JonathonReinhart : 응? 질문의 첫 번째 수정은이 질문과 거의 동일합니다. 당신의 "당신은 왜 관심이 있습니까?" 댓글은 모든 수정 과 관련이 없습니다 . 당신을 공개적으로 꾸짖는 것에 관해서는-당신뿐만 아니라이 문제를 일으키는 많은 사람들이 여기 있습니다. 제목이 "루프에 대한 내부 문 경우 피"하면 , 꽤 분명해야 문제가 제네릭, 그 예는 단지 설명을위한 것입니다 . 질문을 무시하고 그가 사용한 특정 예시적인 예 때문에 OP를 어리석게 보이게 할 때 당신은 누구도 돕지 않습니다.
user541686

답변:


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 이것이 제가 찾던 해결책입니다. 매력처럼 작동합니다. :) 댓글 몇
개로

1
도움이되어 기쁩니다! C ++ 11 코드로 업데이트를 확인하십시오. C ++ 98 버전에 비해 덜 부풀어 있습니다.
Ali

3
Nitpick : 루프 본문이 너무 작기 때문에 OP의 예에서는 괜찮지 만 더 큰 경우 (단지 단일 코드 대신 수십 줄의 코드를 상상해보세요 cout << e << "\n";) 여전히 상당한 코드 중복이있을 것입니다.
syam

3
C ++ 03 예제에서 구조 및 연산자 오버로딩이 사용되는 이유는 무엇입니까? 두 개의 함수를 만들고 포인터를 전달하지 않는 이유는 무엇입니까?
Malcolm

2
@Malcolm 인라이닝. 구조 인 경우 함수 호출이 인라인 될 수 있습니다. 함수 포인터를 전달하면 해당 호출 인라인 할 수 없습니다 .
알리

40

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

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

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


3
이것은 단지 예일뿐입니다. 저는 이런 종류의 문제를 어떻게 해결해야하는지 배우기 위해 여기에 있습니다. 진짜 프로그램을 만들지도 않고 궁금 해서요. 질문에서 언급 했어야했습니다.
Skamah One

40
이 경우 조기 최적화가 모든 악의 근원 임을 명심 하십시오 . 프로그래밍 할 때 항상 코드 가독성에 초점을 맞추고 다른 사람들이 당신이하려는 일을 이해하도록하십시오. 프로그램을 프로파일 링하고 핫스팟을 식별 한 후에 만 마이크로 최적화 및 다양한 해킹 고려 하십시오 . 필요를 설정하지 않고 최적화를 고려해서는 안됩니다. 종종 성능 문제는 예상했던 곳에 있지 않습니다.
Marc Claesen 2013-06-01

3
그리고이 특정 예에서 (알겠습니다. 이것은 단지 예일뿐입니다) 루프 제어에 소요 된 시간과 테스트가 IO에 소요되는 시간 옆에 거의 보이지 않는 경우에 해당 할 가능성이 높습니다. 이것은 종종 C ++의 문제입니다. 유지 관리 비용으로 가독성과 (가상) 효율성 중에서 선택합니다.
kriss

8
코드가 시작될 분기 예측이있는 프로세서에서 실행되고 있다고 가정합니다. C ++를 실행하는 대부분의 시스템은 그렇지 않습니다. (하지만 유용한 기능 std::cout이 있는 대부분의 시스템 )
Ben Voigt

2
-1. 예, 여기서 분기 예측이 잘 작동합니다. 예, 조건은 실제로 컴파일러에 의해 루프 외부에 제기 될 수 있습니다. 네, POITROAE. 그러나 루프 내에서 분기 되어 위험한 일이 종종 사람이 정말 걱정하는 경우 성능에 대한 좋은 조언되어 성능에 영향을, 나는 그냥 "분기 예측"이라고 말하여 사람들을 기각 생각하지 않습니다. 가장 주목할만한 예는 벡터화 컴파일러가 이를 처리하기 위해 예측 이 필요 하여 분기없는 루프보다 효율성이 떨어지는 코드를 생성한다는 것입니다.
Oak

35

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

2
루프 내부의 논리를 더 복잡하게 만드는 대신 코드 중복을 제거했습니다. 이 간단한 예제를 위해 제안한 것보다 나쁘지도 나쁘지도 않습니다. 어쨌든 +1!
알리

1
다른 가능한 최적화를 보여주기 때문에 제안이 마음에 듭니다. 인덱스는 처음부터 템플릿 상수가 될 수 있습니다. 이 경우 writeVector 호출자에 의해 런타임 상수로 대체 될 수 있으며 writeVector는 일부 템플릿으로 변경됩니다. 원래 코드에 대한 추가 변경을 피합니다.
kriss

1
@kriss : 사실 내 이전 솔루션은 이미 doWriteVector직접 전화를 걸면 허용 했지만 이름이 불행 하다는 데 동의합니다. writeVector결과가 더 균질하도록 두 개의 오버로드 된 함수 (하나는 템플릿, 다른 하나는 일반 함수)로 변경했습니다 . 제안 해 주셔서 감사합니다. ;)
syam

4
IMO 이것이 최선의 답변입니다. +1
user541686 2013-06-01

1
@Mehrdad 원래 질문에 답하지 않는다는 점을 제외하고 for 루프 내부에서 if 문을 피합니까? 그래도 성능 저하를 피하는 방법에 대한 답변입니다. "중복"의 경우, 가장 효과적인 방법을 확인하려면 사용 사례가 포함 된보다 현실적인 예가 필요합니다. 이전에 말했듯이이 답변을 찬성했습니다.
알리

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>

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

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.