if… else if 문을 확률 적으로 주문하면 어떤 효과가 있습니까?


187

특히 일련의 if... else if문이 있고 각 문이 평가 할 상대 확률을 미리 알고 true있다면 확률 순서대로 정렬하는 데 실행 시간의 차이가 얼마나됩니까? 예를 들어, 이것을 선호해야합니까?

if (highly_likely)
  //do something
else if (somewhat_likely)
  //do something
else if (unlikely)
  //do something

이에?:

if (unlikely)
  //do something
else if (somewhat_likely)
  //do something
else if (highly_likely)
  //do something

정렬 된 버전이 더 빠를 것 같지만, 가독성이나 부작용의 존재를 위해 최적화되지 않은 순서로 주문할 수 있습니다. 실제로 코드를 실행할 때까지 CPU가 분기 예측을 얼마나 잘 수행하는지 말하기도 어렵습니다.

따라서 이것을 실험하는 과정에서 특정 사례에 대한 내 자신의 질문에 대답했지만 다른 의견 / 통찰력도 듣고 싶습니다.

중요 :이 질문은 if프로그램 동작에 다른 영향을주지 않으면 서 문을 임의로 재정렬 할 수 있다고 가정합니다 . 내 대답에 따르면, 세 가지 조건부 테스트는 상호 배타적이며 부작용이 없습니다. 분명히, 원하는 행동을 달성하기 위해 진술을 일정한 순서로 평가해야한다면 효율성 문제는 문제가된다.


35
그렇지 않으면 두 버전이 동일하지 있으며, 조건이 상호 배타적임을 메모를 추가 할 수 있습니다
idclev 463,035,818

28
한 시간 안에 자기 답이 된 질문이 답이 좋지 않은 20 개 이상의 공감을 얻은 방법이 꽤 흥미 롭습니다. OP에서는 아무것도 부르지 않지만 업보 터는 밴드 왜건에서 점프하는 것을 조심해야합니다. 질문은 흥미로울 수 있지만 결과는 의문입니다.
luk32

3
나는 이것이 하나의 비교를 치는 것이 다른 비교를 치는 것을 거부하기 때문에 단락 평가 의 한 형태로 묘사 될 수 있다고 생각합니다 . 부울이라고 말하면 리소스가 많은 문자열 조작, 정규식 또는 데이터베이스 상호 작용이 포함될 수있는 다른 비교로 들어 가지 못하게 할 수있을 때 개인적으로 이와 같은 구현을 선호합니다.
MonkeyZeus

11
일부 컴파일러는 취해진 분기에 대한 통계를 수집하고이를 더 나은 최적화를 수행 할 수 있도록 컴파일러에 다시 공급하는 기능을 제공합니다.

11
이와 같은 성능이 중요하다면 Profile Guided Optimization을 시도하고 수동 결과와 컴파일러의 결과를 비교해야합니다
Justin

답변:


96

일반적으로 모든 인텔 CPU가 포워드 브랜치를 처음 볼 때 사용하지 않는 것으로 간주하는 것은 아닙니다. Godbolt의 작업을 참조하십시오 .

그 후, 브랜치는 브랜치 예측 캐시로 들어가고, 과거의 브랜치는 예측을 알리기 위해 사용됩니다.

따라서 긴밀한 루프에서 잘못된 순서의 영향은 상대적으로 작습니다. 브랜치 예측기는 어떤 브랜치 세트가 가장 가능성이 높은지 알게 될 것이며 루프에서 사소한 양의 작업을 수행하면 작은 차이가 크게 증가하지 않습니다.

일반적으로 대부분의 컴파일러는 기본적으로 (다른 이유가 없음) 생성 된 머신 코드를 코드에서 주문한 방식과 거의 동일하게 주문합니다. 따라서 문이 실패 할 때 정방향 분기 인 경우.

따라서 "첫 번째 만남"에서 최상의 분기 예측을 얻을 가능성을 줄이는 순서로 분기를 주문해야합니다.

일련의 조건에 걸쳐 여러 번 단단히 반복되고 사소한 작업을 수행하는 마이크로 벤치 마크는 명령 수 등의 작은 영향으로 인해 상대적 분기 예측 문제에는 거의 영향을 미치지 않습니다. 따라서이 경우 경험 법칙이 신뢰할 수 없으므로를 프로파일 링해야합니다 .

또한 벡터화 및 기타 여러 최적화가 작은 타이트 루프에 적용됩니다.

따라서 일반적인 코드에서 가장 가능성이 높은 코드를 if블록 내에 넣으면 캐시되지 않은 분기 예측 누락이 최소화됩니다. 꽉 조이는 경우 일반 규칙에 따라 시작하십시오. 더 많은 정보가 필요하면 프로파일 링 이외의 선택은 거의 없습니다.

일부 테스트가 다른 테스트보다 훨씬 저렴하면 당연히이 모든 것이 창 밖으로 나옵니다.


19
테스트 자체가 얼마나 비싼 지 고려해 볼 가치가 있습니다. 하나의 테스트가 약간 더 가능성이 높지만 많은 경우 더 비싸다면, 다른 테스트를 먼저하는 것이 좋습니다. 분기 예측 등을 통한 절감
psmears

귀하가 제공 한 링크 가 귀하의 결론을 뒷받침하지 않습니다 . 일반적으로 모든 인텔 CPU가 포워드 브랜치를 처음 볼 때이를 취하지 않는 것으로 간주하는 것은 아닙니다. . 사실 결과가 먼저 표시되는 비교적 불분명 한 Arrendale CPU의 경우에만 해당됩니다. 주류 아이비 브릿지와 하 스웰 결과는 전혀 지원하지 않습니다. Haswell은 보이지 않는 가지에 대해 "항상 추락 예측"에 매우 가깝게 보이며 Ivy Bridge는 전혀 명확하지 않습니다.
BeeOnRope

일반적으로 CPU는 과거와 같이 정적 예측을 사용하지 않는 것으로 이해됩니다. 실제로 현대 인텔은 아마도 확률 적 TAGE 예측 변수와 같은 것을 사용하고있을 것입니다. 분기 히스토리를 다양한 히스토리 테이블로 해시하고 가장 긴 히스토리와 일치하는 것을 가져옵니다. 앨리어싱을 피하기 위해 "태그"를 사용하지만 태그에는 몇 비트 만 있습니다. 모든 히스토리 길이를 놓치면 반드시 분기 방향에 의존하지 않는 기본 예측이 이루어집니다 (Haswell에서는 분명히 그렇지 않다고 말할 수 있음).
BeeOnRope

44

나는 두 가지 다른 if... else if블록 의 실행 시간을 정하기 위해 다음 테스트를 구성했습니다. 하나는 확률 순서로 정렬되고 다른 하나는 역순으로 정렬되었습니다.

#include <chrono>
#include <iostream>
#include <random>
#include <algorithm>
#include <iterator>
#include <functional>

using namespace std;

int main()
{
    long long sortedTime = 0;
    long long reverseTime = 0;

    for (int n = 0; n != 500; ++n)
    {
        //Generate a vector of 5000 random integers from 1 to 100
        random_device rnd_device;
        mt19937 rnd_engine(rnd_device());
        uniform_int_distribution<int> rnd_dist(1, 100);
        auto gen = std::bind(rnd_dist, rnd_engine);
        vector<int> rand_vec(5000);
        generate(begin(rand_vec), end(rand_vec), gen);

        volatile int nLow, nMid, nHigh;
        chrono::time_point<chrono::high_resolution_clock> start, end;

        //Sort the conditional statements in order of increasing likelyhood
        nLow = nMid = nHigh = 0;
        start = chrono::high_resolution_clock::now();
        for (int& i : rand_vec) {
            if (i >= 95) ++nHigh;               //Least likely branch
            else if (i < 20) ++nLow;
            else if (i >= 20 && i < 95) ++nMid; //Most likely branch
        }
        end = chrono::high_resolution_clock::now();
        reverseTime += chrono::duration_cast<chrono::nanoseconds>(end-start).count();

        //Sort the conditional statements in order of decreasing likelyhood
        nLow = nMid = nHigh = 0;
        start = chrono::high_resolution_clock::now();
        for (int& i : rand_vec) {
            if (i >= 20 && i < 95) ++nMid;  //Most likely branch
            else if (i < 20) ++nLow;
            else if (i >= 95) ++nHigh;      //Least likely branch
        }
        end = chrono::high_resolution_clock::now();
        sortedTime += chrono::duration_cast<chrono::nanoseconds>(end-start).count();

    }

    cout << "Percentage difference: " << 100 * (double(reverseTime) - double(sortedTime)) / double(sortedTime) << endl << endl;
}

/ O2와 함께 MSVC2017을 사용하면 정렬 된 버전이 정렬되지 않은 버전보다 일관되게 약 28 % 빠릅니다. luk32의 의견에 따라, 나는 또한 두 테스트의 순서를 바꾸어 눈에 띄는 차이를 만듭니다 (22 % 대 28 %). 이 코드는 Intel Xeon E5-2697 v2의 Windows 7에서 실행되었습니다. 이것은 물론 문제에 따라 다르며 결정적인 답변으로 해석되어서는 안됩니다.


9
if... else if명령문 을 변경하면 로직이 코드를 통과하는 방식에 상당한 영향을 줄 수 있으므로 OP는주의해야합니다 . unlikely검사는 자주 오지 않을 수 있지만, 확인하는 비즈니스 요구 사항이있을 수 있습니다 unlikely먼저 다른 사람들을 점검하기 전에 조건.
Luke T Brooks

21
30 % 더 빠릅니까? 여분의 if 문이 수행 할 필요가없는 %의 비율로 더 빠르다는 것을 의미합니까? 꽤 합리적인 결과 인 것 같습니다.
UKMonkey

5
그것을 어떻게 벤치마킹 했습니까? 어떤 컴파일러, CPU 등? 이 결과가 이식 가능하지 않다고 확신합니다.
luk32

12
이 마이크로 벤치 마크의 문제점은 CPU가 어느 분기가 가장 가능성이 높은지 알아 내고 반복적으로 반복 할 때이를 캐시한다는 것입니다. 작은 타이트 루프에서 검사하지 않는 분기의 경우 분기 예측 캐시에 분기가 없을 수 있으며 CPU가 분기 예측 캐시 지침이 0으로 잘못 추측하면 비용이 훨씬 높아질 수 있습니다.
Yakk-Adam Nevraumont

6
이 벤치 마크는 너무 신뢰할 수 없습니다. gcc 6.3.0으로 컴파일 : g++ -O2 -march=native -std=c++14정렬 된 조건문에 약간의 우위를 제공하지만 대부분의 경우 두 실행 간의 백분율 차이는 ~ 5 %입니다. 여러 번, 차이 때문에 실제로 느려졌습니다. 나는 if이런 식으로 주문하는 것이 걱정할 가치가 없다고 확신한다 . PGO는 아마도 모든 경우를 완벽하게 처리 할 것입니다
Justin

30

대상 시스템이 실제로 영향을받는 것이 확실하지 않으면 안됩니다. 기본적으로 가독성이 좋습니다.

나는 당신의 결과를 의심합니다. 예제를 약간 수정 했으므로 실행을 취소하는 것이 더 쉽습니다. 오히려 Ideone 은 역순이 빠르지 만 일관성있게 보여줍니다. 특정 달리기에서도 때때로 뒤집어집니다. 결과가 결정적이지 않다고 말하고 싶습니다. 콜리 루 는 실제 차이도보고하지 않습니다. 나중에 내 odroid xu4에서 Exynos5422 CPU를 확인할 수 있습니다.

현대 CPU에는 분기 예측기가 있습니다. 데이터와 명령어를 모두 프리 페치 (pre-fetch)하기위한 많은 로직이 있으며, 최신 x86 CPU는 다소 똑똑합니다. ARM 또는 GPU와 같은 일부 슬림 아키텍처는이 취약점에 취약 할 수 있습니다. 그러나 실제로 컴파일러와 대상 시스템에 크게 의존합니다.

지점 순서 최적화는 매우 취약하고 임시적이라고 말할 수 있습니다. 정말 미세 조정 단계로만 수행하십시오.

암호:

#include <chrono>
#include <iostream>
#include <random>
#include <algorithm>
#include <iterator>
#include <functional>

using namespace std;

int main()
{
    //Generate a vector of random integers from 1 to 100
    random_device rnd_device;
    mt19937 rnd_engine(rnd_device());
    uniform_int_distribution<int> rnd_dist(1, 100);
    auto gen = std::bind(rnd_dist, rnd_engine);
    vector<int> rand_vec(5000);
    generate(begin(rand_vec), end(rand_vec), gen);
    volatile int nLow, nMid, nHigh;

    //Count the number of values in each of three different ranges
    //Run the test a few times
    for (int n = 0; n != 10; ++n) {

        //Run the test again, but now sort the conditional statements in reverse-order of likelyhood
        {
          nLow = nMid = nHigh = 0;
          auto start = chrono::high_resolution_clock::now();
          for (int& i : rand_vec) {
              if (i >= 95) ++nHigh;               //Least likely branch
              else if (i < 20) ++nLow;
              else if (i >= 20 && i < 95) ++nMid; //Most likely branch
          }
          auto end = chrono::high_resolution_clock::now();
          cout << "Reverse-sorted: \t" << chrono::duration_cast<chrono::nanoseconds>(end-start).count() << "ns" << endl;
        }

        {
          //Sort the conditional statements in order of likelyhood
          nLow = nMid = nHigh = 0;
          auto start = chrono::high_resolution_clock::now();
          for (int& i : rand_vec) {
              if (i >= 20 && i < 95) ++nMid;  //Most likely branch
              else if (i < 20) ++nLow;
              else if (i >= 95) ++nHigh;      //Least likely branch
          }
          auto end = chrono::high_resolution_clock::now();
          cout << "Sorted:\t\t\t" << chrono::duration_cast<chrono::nanoseconds>(end-start).count() << "ns" << endl;
        }
        cout << endl;
    }
}

코드에서와 같이 정렬 및 역 정렬 된 if-block의 순서를 전환하면 성능이 ~ 30 % 차이가납니다. 왜 Ideone과 coliru가 차이를 보이지 않는지 잘 모르겠습니다.
Carlton

확실히 흥미 롭습니다. 다른 시스템에 대한 일부 데이터를 가져 오려고 시도하지만 데이터를 처리해야 할 때까지 하루가 걸릴 수 있습니다. 이 질문은 특히 결과에 비추어 볼 때 흥미 롭습니다. 그러나 그것들은 너무나 훌륭하여 그것을 교차 확인해야했습니다.
luk32

질문이 효과 라면 ? 대답은 아니오 일없습니다 !
PJTraill

예. 하지만 원래 질문에 대한 업데이트 알림을받지 못했습니다. 그들은 답변 형식을 쓸모 없게 만들었습니다. 죄송합니다. 나중에 내용을 편집하여 원래 질문에 대답하고 원래 요점을 입증 한 결과를 보여 드리겠습니다.
luk32

"기본적으로 가독성으로 이동합니다." 읽을 수있는 코드를 작성하면 사람이 구문 분석하기 어려운 코드를 만들어서 약간의 성능 향상 (절대적인 용어로)을 시도하는 것보다 더 나은 결과를 얻을 수 있습니다.
Andrew Brēza

26

그냥 내 5 센트. 진술이 다음에 의존해야하는 경우 주문의 효과가있는 것 같습니다.

  1. 각 if 문의 가능성

  2. 분기 예측자가 시작할 수있는 반복 횟수입니다.

  3. 코드 힌트와 같은 컴파일러 힌트

이러한 요소를 탐색하기 위해 다음 기능을 벤치마킹했습니다.

ordered_ifs ()

for (i = 0; i < data_sz * 1024; i++) {
    if (data[i] < check_point) // highly likely
        s += 3;
    else if (data[i] > check_point) // samewhat likely
        s += 2;
    else if (data[i] == check_point) // very unlikely
        s += 1;
}

reversed_ifs ()

for (i = 0; i < data_sz * 1024; i++) {
    if (data[i] == check_point) // very unlikely
        s += 1;
    else if (data[i] > check_point) // samewhat likely
        s += 2;
    else if (data[i] < check_point) // highly likely
        s += 3;
}

ordered_ifs_with_hints ()

for (i = 0; i < data_sz * 1024; i++) {
    if (likely(data[i] < check_point)) // highly likely
        s += 3;
    else if (data[i] > check_point) // samewhat likely
        s += 2;
    else if (unlikely(data[i] == check_point)) // very unlikely
        s += 1;
}

reversed_ifs_with_hints ()

for (i = 0; i < data_sz * 1024; i++) {
    if (unlikely(data[i] == check_point)) // very unlikely
        s += 1;
    else if (data[i] > check_point) // samewhat likely
        s += 2;
    else if (likely(data[i] < check_point)) // highly likely
        s += 3;
}

데이터

데이터 배열은 0에서 100 사이의 난수를 포함합니다.

const int RANGE_MAX = 100;
uint8_t data[DATA_MAX * 1024];

static void data_init(int data_sz)
{
    int i;
        srand(0);
    for (i = 0; i < data_sz * 1024; i++)
        data[i] = rand() % RANGE_MAX;
}

결과

다음 결과는 Intel i5 @ 3,2 GHz 및 G ++ 6.3.0에 대한 것입니다. 첫 번째 인수는 check_point (즉, 가능성이 높은 if 문의 경우 %% 확률)이고 두 번째 인수는 data_sz (예 : 반복 횟수)입니다.

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/4                    4660 ns       4658 ns     150948
ordered_ifs/50/8                   25636 ns      25635 ns      27852
ordered_ifs/75/4                    4326 ns       4325 ns     162613
ordered_ifs/75/8                   18242 ns      18242 ns      37931
ordered_ifs/100/4                   1673 ns       1673 ns     417073
ordered_ifs/100/8                   3381 ns       3381 ns     207612
reversed_ifs/50/4                   5342 ns       5341 ns     126800
reversed_ifs/50/8                  26050 ns      26050 ns      26894
reversed_ifs/75/4                   3616 ns       3616 ns     193130
reversed_ifs/75/8                  15697 ns      15696 ns      44618
reversed_ifs/100/4                  3738 ns       3738 ns     188087
reversed_ifs/100/8                  7476 ns       7476 ns      93752
ordered_ifs_with_hints/50/4         5551 ns       5551 ns     125160
ordered_ifs_with_hints/50/8        23191 ns      23190 ns      30028
ordered_ifs_with_hints/75/4         3165 ns       3165 ns     218492
ordered_ifs_with_hints/75/8        13785 ns      13785 ns      50574
ordered_ifs_with_hints/100/4        1575 ns       1575 ns     437687
ordered_ifs_with_hints/100/8        3130 ns       3130 ns     221205
reversed_ifs_with_hints/50/4        6573 ns       6572 ns     105629
reversed_ifs_with_hints/50/8       27351 ns      27351 ns      25568
reversed_ifs_with_hints/75/4        3537 ns       3537 ns     197470
reversed_ifs_with_hints/75/8       16130 ns      16130 ns      43279
reversed_ifs_with_hints/100/4       3737 ns       3737 ns     187583
reversed_ifs_with_hints/100/8       7446 ns       7446 ns      93782

분석

1. 주문이 중요하다

4K 반복과 (거의) 100 % 확률로 선호도가 높은 진술의 경우 그 차이는 223 %에 달합니다.

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/100/4                   1673 ns       1673 ns     417073
reversed_ifs/100/4                  3738 ns       3738 ns     188087

4K 반복 및 선호도가 높은 50 % 확률의 경우 차이는 약 14 %입니다.

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/4                    4660 ns       4658 ns     150948
reversed_ifs/50/4                   5342 ns       5341 ns     126800

2. 반복 횟수가 중요

매우 좋아하는 진술의 (거의) 100 % 확률에 대한 4K와 8K 반복의 차이는 (예상대로) 약 2 배입니다.

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/100/4                   1673 ns       1673 ns     417073
ordered_ifs/100/8                   3381 ns       3381 ns     207612

그러나 선호도가 높은 문장의 50 % 확률에 대한 4K와 8K 반복의 차이는 5,5 배입니다.

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/4                    4660 ns       4658 ns     150948
ordered_ifs/50/8                   25636 ns      25635 ns      27852

왜 그렇습니까? 분기 예측 변수가 누락되었습니다. 위에서 언급 한 각 사례에 대한 분기 누락이 있습니다.

ordered_ifs/100/4    0.01% of branch-misses
ordered_ifs/100/8    0.01% of branch-misses
ordered_ifs/50/4     3.18% of branch-misses
ordered_ifs/50/8     15.22% of branch-misses

따라서 i5에서 브랜치 예측기는 그렇게 크지 않은 브랜치 및 대규모 데이터 세트에 대해 놀랍도록 실패합니다.

힌트 힌트

4K 반복의 경우 결과는 50 % 확률에서는 다소 나쁘고 100 % 확률에 대해서는 다소 나아집니다.

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/4                    4660 ns       4658 ns     150948
ordered_ifs/100/4                   1673 ns       1673 ns     417073
ordered_ifs_with_hints/50/4         5551 ns       5551 ns     125160
ordered_ifs_with_hints/100/4        1575 ns       1575 ns     437687

그러나 8K 반복의 경우 결과가 항상 조금 더 좋습니다.

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/8                   25636 ns      25635 ns      27852
ordered_ifs/100/8                   3381 ns       3381 ns     207612
ordered_ifs_with_hints/50/8        23191 ns      23190 ns      30028
ordered_ifs_with_hints/100/8        3130 ns       3130 ns     221205

따라서 힌트도 도움이되지만 조금만 도움이됩니다.

결론은 항상 코드를 벤치마킹한다는 것입니다. 결과는 놀라 울 수 있습니다.

희망이 도움이됩니다.


1
i5 네 할렘? i5 스카이 레이크? "i5"라고 말하는 것은 그리 구체적이지 않습니다. 또한, 당신이 사용하는 가정 g++ -O2이나 -O3 -fno-tree-vectorize,하지만 당신은 그렇게 말한다.
Peter Cordes

with_hints가 여전히 순서와 반대로 다르다는 점에 흥미가 있습니다. 어딘가에 소스에 연결하면 좋을 것입니다. (예를 들어 Godbolt 링크, 바람직하게는 전체 링크이므로 링크 단축은 썩을 수 없습니다.)
Peter Cordes

1
브랜치 예측자가 4K 입력 데이터 크기에서도 잘 예측할 수 있다는 사실, 즉 수천 의 기간 동안 루프에서 브랜치 결과를 기억함으로써 벤치 마크를 "파괴"할 수 있다는 사실 은 현대의 힘에 대한 증거입니다 분기 예측 변수. 예측 변수는 경우에 따라 정렬과 같은 것에 매우 민감하므로 일부 변경에 대한 강력한 결론을 내리기가 어렵습니다. 예를 들어, 다른 경우에 힌트에 대해 반대 동작을 발견했지만 예측 변수에 영향을주는 코드 레이아웃을 임의로 변경하여 설명 할 수 있습니다.
BeeOnRope 23

1
@PeterCordes 나의 주요 요점은 변화의 결과를 예측하려고 시도하지만 변화 전후의 성능을 더 잘 측정하는 것입니다 ... 그리고 당신은 맞습니다 .-O3와 프로세서에 최적화되어 있다고 언급했습니다 i5-4460 @ 3.20GHz
Andriy Berestovskyy

19

유일한 진짜 답이 같이 여기에 다른 몇 가지 답변을 바탕으로, 그것은 본다 : 그것은 의존한다 . 그것은 적어도 다음에 달려 있습니다 (그러나이 순서대로 중요하지는 않지만).

  • 각 지점의 상대 확률. 이것은 원래 질문입니다. 기존의 답변을 바탕으로 확률 순으로 정렬하는 데 도움이되는 몇 가지 조건이있는 것처럼 보이지만 항상 그렇지는 않습니다. 상대 확률이 크게 다르지 않은 경우 순서가 어떤 순서로든 차이가 나지 않을 수 있습니다. 그러나 첫 번째 조건이 99.999 %의 시간에 발생하고 다음 조건이 남은 것의 일부인 경우에는 가장 가능성이 높은 것을 먼저 배치하는 것이 타이밍 측면에서 유리하다고 가정합니다.
  • 각 브랜치에 대한 참 / 거짓 조건 계산 비용. 조건을 테스트하는 데 드는 시간 비용이 한 지점에서 다른 지점에 비해 실제로 높은 경우 이는 타이밍과 효율성에 큰 영향을 줄 수 있습니다. 예를 들어, 계산하는 데 1 시간 단위 (예 : 부울 변수의 상태 확인)가 걸리는 조건과 계산하는 데 수십, 수백, 수천 또는 심지어 수백만 시간 단위가 필요한 다른 조건 (예 : 디스크상의 파일 또는 대규모 데이터베이스에 대한 복잡한 SQL 쿼리 수행). 코드가 매번 순서대로 조건을 확인한다고 가정하면 더 빠른 조건이 먼저되어야합니다 (먼저 실패한 다른 조건에 종속되지 않는 한).
  • 컴파일러 / 인터프리터 일부 컴파일러 (또는 인터프리터)에는 성능에 영향을 줄 수있는 다른 종류의 최적화가 포함될 수 있습니다 (일부 옵션은 컴파일 및 / 또는 실행 중에 특정 옵션을 선택한 경우에만 나타남). 따라서 동일한 분기를 정확히 동일한 컴파일러를 사용하여 동일한 시스템에서 두 개의 컴파일 및 달리 동일한 코드의 실행을 벤치마킹하지 않는 한 유일한 차이점은 컴파일러 변형에 대한 약간의 여유를 주어야합니다.
  • 운영 체제 / 하드웨어 luk32 및 Yakk에서 언급했듯이 다양한 CPU에는 운영 체제와 마찬가지로 자체 최적화 기능이 있습니다. 따라서 벤치 마크는 여기서도 변형되기 쉽습니다.
  • 코드 블록 실행 빈도 분기를 포함하는 블록에 거의 액세스하지 않는 경우 (예 : 시작 중 한 번만) 분기를 배치하는 순서는 거의 중요하지 않습니다. 다른 한편으로, 코드의 중요한 부분에서 코드가이 코드 블록을 망치는 경우 벤치 마크에 따라 순서가 중요 할 수 있습니다.

확실하게 알 수있는 유일한 방법은 코드를 최종적으로 실행할 의도 된 시스템과 동일하거나 매우 유사한 시스템에서 특정 사례를 벤치마킹하는 것입니다. 하드웨어, 운영 체제 등이 다른 다양한 시스템에서 실행하려는 경우 여러 변형을 벤치 마크하여 어느 것이 가장 적합한 지 확인하는 것이 좋습니다. 한 유형의 시스템에서는 하나의 순서로, 다른 유형의 시스템에서는 다른 순서로 코드를 컴파일하는 것이 좋습니다.

내 개인적인 경험 규칙 (대부분의 경우 벤치 마크가없는 경우)은 다음을 기준으로 주문해야합니다.

  1. 이전 조건의 결과에 의존하는 조건
  2. 조건 계산 비용
  3. 각 지점의 상대 확률.

13

필자가 일반적으로 고성능 코드에 대해이 문제를 해결하는 방법은 가장 읽기 쉬운 순서를 유지하지만 컴파일러에 힌트를 제공하는 것입니다. 다음은 Linux 커널의 한 예입니다 .

if (likely(access_ok(VERIFY_READ, from, n))) {
    kasan_check_write(to, n);
    res = raw_copy_from_user(to, from, n);
}
if (unlikely(res))
    memset(to + (n - res), 0, res);

여기서는 액세스 확인이 통과되고에 오류가 반환되지 않는다고 가정 res합니다. 이러한 if 절 중 하나를 재정렬하려고하면 코드가 혼동 될 수 있지만, 매크로 likely()unlikely()매크로는 실제로 정상적인 경우와 예외는 무엇인지 지적함으로써 가독성을 향상시킵니다.

이러한 매크로의 Linux 구현은 GCC 특정 기능을 사용 합니다 . clang과 Intel C 컴파일러는 동일한 구문을 지원하는 것으로 보이지만 MSVC에는 그러한 기능이 없습니다 .


4
매크로 likely()unlikely()매크로 정의 방법을 설명 하고 해당 컴파일러 기능에 대한 정보를 포함 할 수 있으면 더욱 유용 합니다.
Nate Eldredge

1
AFAIK, 이러한 힌트는 "단독"으로 코드 블록의 메모리 레이아웃을 변경하고 yes 또는 no가 점프로 이어질지 여부를 변경합니다. 이것은 예를 들어 메모리 페이지를 읽을 필요성 (또는 부족)에 대한 성능 이점을 가질 수있다. 그러나 이것은 다른 else-if의 긴 목록 내의 조건이 평가되는 순서를 재정렬하지는 않습니다.
Hagen von Eitzen

@HagenvonEitzen Hmm 네, 좋은 지적입니다 else if. 컴파일러가 조건이 상호 배타적이라는 것을 알만큼 똑똑하지 않은 경우 순서에 영향을 줄 수 없습니다 .
jpa

7

또한 컴파일러와 컴파일하는 플랫폼에 따라 다릅니다.

이론적으로 가장 가능성이 높은 조건은 제어 점프를 가능한 한 적게해야합니다.

일반적으로 가장 가능성이 높은 조건은 다음과 같습니다.

if (most_likely) {
     // most likely instructions
} else 

가장 인기있는 asm은 condition이 true 일 때 점프하는 조건부 분기를 기반으로 합니다 . 그 C 코드는 다음과 같은 의사 asm으로 변환 될 것입니다.

jump to ELSE if not(most_likely)
// most likely instructions
jump to end
ELSE:

이것은 점프가 CPU가 실행 파이프 라인을 취소하고 프로그램 카운터가 변경 되었기 때문에 중단되기 때문입니다 (실제로는 파이프 라인을 지원하는 아키텍처의 경우). 그런 다음 컴파일러에 관한 것인데, 이는 컨트롤이 덜 점프하도록 통계적으로 가장 가능성이 높은 조건을 갖는 것에 대한 정교한 최적화를 적용하거나 적용하지 않을 수 있습니다.


2
조건이 참일 때 조건부 분기가 발생한다고 말했지만 "의사 asm"예는 그 반대입니다. 또한 현대 CPU에는 일반적으로 분기 예측이 있기 때문에 조건부 점프 (모든 점프가 훨씬 적음)가 파이프 라인을 정지 시킨다고 말할 수 없습니다. 실제로 분기가 수행 될 것으로 예상되지만 취해지지 않으면 파이프 라인이 정지됩니다. 나는 아직도 확률의 내림차순으로 조건을 정렬하려고하지만 그것의 컴파일러와 CPU 메이크업 것은 것입니다 매우 구현에 의존합니다.
Arne Vogel

1
“not (most_likely)”를 넣었으므로 most_likely가 true이면 컨트롤은 점프하지 않고 진행됩니다.
NoImaginationGuy

1
"가장 인기있는 asm은 조건이 참일 때 점프하는 조건부 분기를 기반으로합니다."어떤 ISA가 될까요? x86이나 ARM에는 해당되지 않습니다. 지옥 분기 예측은 앞으로 분기가되어 있다고 가정합니다 (그들은 일반적으로 여전히 가정에서 시작하고 적응 복잡한 BPS과 매우 고대의 x86 것들) 기본 ARM의 CPU에 대한 하지 청구의 반대 때문에, 촬영 및 하위 가지가 항상 사실이다.
Voo

1
내가 시도한 대부분 의 컴파일러 는 간단한 테스트를 위해 위에서 언급 한 접근법을 사용했습니다. 주 clang사실에 대한 다른 접근 방식을했다 test2test3: 때문에 나타냅니다 추론의 < 0또는 == 0테스트 가능성이 거짓이 될 것입니다, 그것은 할 수 있도록, 두 경로에 함수의 나머지 부분을 복제하기로 결정 condition == false경로를 통해 가을. 이것은 함수의 나머지 부분이 짧기 때문에 가능합니다. test4한 번 더 작업을 추가하고 위에서 설명한 접근 방식으로 돌아갑니다.
BeeOnRope

1
@ArneVogel-올바르게 예측 된 분기는 최신 CPU에서 파이프 라인을 완전히 정지 시키지는 않지만 여전히 그렇지 않은 것보다 훨씬 더 나쁩니다. (1) 제어 흐름이 연속적이지 않으므로 나머지 명령 jmp은 그렇지 않습니다. 유용하므로 페치 / 디코딩 대역폭이 낭비됩니다 (2) 심지어 최신 빅 코어는 사이클 당 하나의 페치 만 수행하므로 1 분기 / 사이클 당 하드 제한을 적용합니다 (OTOH 현대 인텔은 2 개의 미 취득 / 사이클을 수행 할 수 있음) (3 )는 ... 촬영 연속 지점으로 빠르게 + 느린 예측의 경우 거래에 분기 예측을 위해 더 어려워
BeeOnRope

6

Lik32 코드를 사용하여 내 컴퓨터에서 테스트를 다시 실행하기로 결정했습니다. 높은 해상도가 1ms라고 생각하는 내 창이나 컴파일러로 인해 변경해야했습니다.

mingw32-g ++. exe -O3-벽 -std = c ++ 11 -fexceptions -g

vector<int> rand_vec(10000000);

GCC는 두 원본 코드에서 동일한 변환을 수행했습니다.

세 번째 조건이 항상 맞아야하기 때문에 두 가지 첫 번째 조건 만 테스트합니다. GCC는 일종의 셜록입니다.

역전

.L233:
        mov     DWORD PTR [rsp+104], 0
        mov     DWORD PTR [rsp+100], 0
        mov     DWORD PTR [rsp+96], 0
        call    std::chrono::_V2::system_clock::now()
        mov     rbp, rax
        mov     rax, QWORD PTR [rsp+8]
        jmp     .L219
.L293:
        mov     edx, DWORD PTR [rsp+104]
        add     edx, 1
        mov     DWORD PTR [rsp+104], edx
.L217:
        add     rax, 4
        cmp     r14, rax
        je      .L292
.L219:
        mov     edx, DWORD PTR [rax]
        cmp     edx, 94
        jg      .L293 // >= 95
        cmp     edx, 19
        jg      .L218 // >= 20
        mov     edx, DWORD PTR [rsp+96]
        add     rax, 4
        add     edx, 1 // < 20 Sherlock
        mov     DWORD PTR [rsp+96], edx
        cmp     r14, rax
        jne     .L219
.L292:
        call    std::chrono::_V2::system_clock::now()

.L218: // further down
        mov     edx, DWORD PTR [rsp+100]
        add     edx, 1
        mov     DWORD PTR [rsp+100], edx
        jmp     .L217

And sorted

        mov     DWORD PTR [rsp+104], 0
        mov     DWORD PTR [rsp+100], 0
        mov     DWORD PTR [rsp+96], 0
        call    std::chrono::_V2::system_clock::now()
        mov     rbp, rax
        mov     rax, QWORD PTR [rsp+8]
        jmp     .L226
.L296:
        mov     edx, DWORD PTR [rsp+100]
        add     edx, 1
        mov     DWORD PTR [rsp+100], edx
.L224:
        add     rax, 4
        cmp     r14, rax
        je      .L295
.L226:
        mov     edx, DWORD PTR [rax]
        lea     ecx, [rdx-20]
        cmp     ecx, 74
        jbe     .L296
        cmp     edx, 19
        jle     .L297
        mov     edx, DWORD PTR [rsp+104]
        add     rax, 4
        add     edx, 1
        mov     DWORD PTR [rsp+104], edx
        cmp     r14, rax
        jne     .L226
.L295:
        call    std::chrono::_V2::system_clock::now()

.L297: // further down
        mov     edx, DWORD PTR [rsp+96]
        add     edx, 1
        mov     DWORD PTR [rsp+96], edx
        jmp     .L224

따라서 마지막 경우에는 분기 예측이 필요하지 않다는 점을 제외하고는 많은 것을 알려주지 않습니다.

이제 if의 6 가지 조합을 모두 시도했지만 상단 2는 원래 역순으로 정렬되었습니다. high는> = 95, low는 <20, mid는 각각 10000000 반복으로 20-94입니다.

high, low, mid: 43000000ns
mid, low, high: 46000000ns
high, mid, low: 45000000ns
low, mid, high: 44000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns

high, low, mid: 44000000ns
mid, low, high: 47000000ns
high, mid, low: 44000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 45000000ns

high, low, mid: 43000000ns
mid, low, high: 47000000ns
high, mid, low: 44000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns

high, low, mid: 42000000ns
mid, low, high: 46000000ns
high, mid, low: 46000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 43000000ns

high, low, mid: 43000000ns
mid, low, high: 47000000ns
high, mid, low: 44000000ns
low, mid, high: 44000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns

high, low, mid: 43000000ns
mid, low, high: 48000000ns
high, mid, low: 44000000ns
low, mid, high: 44000000ns
mid, high, low: 45000000ns
low, high, mid: 45000000ns

high, low, mid: 43000000ns
mid, low, high: 47000000ns
high, mid, low: 45000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns

high, low, mid: 43000000ns
mid, low, high: 47000000ns
high, mid, low: 45000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns

high, low, mid: 43000000ns
mid, low, high: 46000000ns
high, mid, low: 45000000ns
low, mid, high: 45000000ns
mid, high, low: 45000000ns
low, high, mid: 44000000ns

high, low, mid: 42000000ns
mid, low, high: 46000000ns
high, mid, low: 44000000ns
low, mid, high: 45000000ns
mid, high, low: 45000000ns
low, high, mid: 44000000ns

1900020, 7498968, 601012

Process returned 0 (0x0)   execution time : 2.899 s
Press any key to continue.

그렇다면 왜 순서가 높고 낮고 메드가 더 빠르며 (마지막으로)

가장 예측할 수없는 것이 마지막이므로 분기 예측자를 통해 실행되지 않습니다.

          if (i >= 95) ++nHigh;               // most predictable with 94% taken
          else if (i < 20) ++nLow; // (94-19)/94% taken ~80% taken
          else if (i >= 20 && i < 95) ++nMid; // never taken as this is the remainder of the outfalls.

따라서 가지를 예측하고 가져 가고 나머지를

6 % + (0.94 *) 20 %의 오해.

"정렬"

          if (i >= 20 && i < 95) ++nMid;  // 75% not taken
          else if (i < 20) ++nLow;        // 19/25 76% not taken
          else if (i >= 95) ++nHigh;      //Least likely branch

가지가 취해지지 않고 셜록으로 예측됩니다.

25 % + (0.75 *) 24 %의 오해

18-23 %의 차이 (측정 된 차이는 ~ 9 %)를 주지만 %를 잘못 예측하는 대신주기를 계산해야합니다.

Nehalem CPU에서 17 사이클이 페널티를 잘못 예측한다고 가정하고 각 검사에서 발행하는 데 1 사이클이 걸리고 (4-5 명령) 루프도 1 사이클이 걸린다고 가정 해 봅시다. 데이터 의존성은 카운터와 루프 변수이지만, 일단 잘못 예측하면 시간에 영향을 미치지 않아야합니다.

따라서 "역"의 경우 타이밍을 얻습니다 (컴퓨터 아키텍처 : 양적 접근 방식 IIRC에서 사용되는 공식이어야 함).

mispredict*penalty+count+loop
0.06*17+1+1+    (=3.02)
(propability)*(first check+mispredict*penalty+count+loop)
(0.19)*(1+0.20*17+1+1)+  (= 0.19*6.4=1.22)
(propability)*(first check+second check+count+loop)
(0.75)*(1+1+1+1) (=3)
= 7.24 cycles per iteration

"정렬"과 동일

0.25*17+1+1+ (=6.25)
(1-0.75)*(1+0.24*17+1+1)+ (=.25*7.08=1.77)
(1-0.75-0.19)*(1+1+1+1)  (= 0.06*4=0.24)
= 8.26

(8.26-7.24) /8.26 = 13.8 % 대 ~ 9 % 측정 (측정 값에 근접!?!).

따라서 OP의 분명함은 분명하지 않습니다.

이러한 테스트를 통해 더 복잡한 코드 또는 더 많은 데이터 종속성을 가진 다른 테스트는 확실히 다르므로 사례를 측정하십시오.

테스트 순서를 변경하면 결과가 변경되었지만 루프 시작의 정렬이 다르기 때문에 모든 최신 Intel CPU에서 16 바이트로 정렬되어야하지만이 경우에는 그렇지 않습니다.


4

원하는 순서대로 논리 순서를 정하십시오. 물론 지점이 느려질 수 있지만 분기가 컴퓨터에서 수행하는 대부분의 작업이되어서는 안됩니다.

코드의 성능이 중요한 부분을 작업하는 경우 논리적 순서, 프로파일 가이드 최적화 및 기타 기술을 사용하지만 일반적인 코드의 경우 실제로 스타일을 더 많이 선택한다고 생각합니다.


6
지점 예측 실패는 비쌉니다. 마이크로 벤치 마크에서는 x86s에 큰 분기 예측 변수 테이블이 있기 때문에 비용 이 많이 듭니다. 동일한 조건에서 루프를 반복하면 CPU가 가장 가능성이 높은 것보다 CPU를 더 잘 알게됩니다. 그러나 코드 전체에 브랜치가있는 경우 브랜치 예측 캐시에 슬롯이 부족할 수 있으며 CPU는 기본값을 가정합니다. 기본 추측이 무엇인지 알면 코드베이스 전체의주기를 줄일 수 있습니다.
Yakk-Adam Nevraumont

@Yakk Jack의 대답은 여기에 유일한 대답입니다. 컴파일러에서 최적화를 수행 할 수있는 경우 가독성을 낮추는 최적화를 수행하지 마십시오. 컴파일러가 당신을 위해 끊임없이 접는 것, 데드 코드 제거, 루프 언 롤링 또는 다른 최적화를하지 않을 것입니까? 코드를 작성하고 프로파일 가이드 최적화 (코더가 추측 할 때이 문제를 해결하기위한 설계)를 사용한 다음 컴파일러가 최적화하는지 확인하십시오. 결국 당신은 어쨌든 성능 크리티컬 코드의 분기를 원하지 않습니다.
Christoph Diegelmann

@Christoph 나는 죽었다고 알고있는 코드를 포함하지 않을 것입니다. 일부 반복자에 대해서는 최적화하기가 어렵고 (나를 위해) 차이는 중요하지 않다는 것을 알고 있기 때문에 i++언제 사용 하지 않을 것입니다. 이것은 비관을 피하는 것입니다. 기본 습관 으로 가장 가능성이 높은 블록을 가장 먼저 설정해도 눈에 띄는 가독성이 떨어지지 않으며 실제로 도움이 될 수 있습니다. 나중에 마이크로 최적화에 의해)++ii++++i
Yakk-Adam Nevraumont

3

if-else 문의 상대 확률을 이미 알고 있다면 성능을 위해 정렬 된 방식을 사용하는 것이 좋습니다. 단 하나의 조건 (진실한 조건) 만 검사하기 때문입니다.

정렬되지 않은 방식으로 컴파일러는 모든 조건을 불필요하게 확인하고 시간이 걸립니다.

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