Является ли разумным отметить только часть выражения как вероятное()/маловероятное()

Предположим, что у меня есть выражение, которое только часть очень маловероятно, но другое является статистически нейтральным:

if (could_be || very_improbable) {
    DoSomething();
}

Помогло бы это компилятору каким-либо образом, если я поместил бы очень невероятный бит в макрос unlikely()?

if (could_be || unlikely(very_improbable)) {
    DoSomething();
}

Примечание. Я не спрашиваю, как работают маркос - я это понимаю. Вопрос здесь в отношении GCC и он сможет оптимизировать выражение, если я только намекаю на его часть. Я также признаю, что это может сильно зависеть от рассматриваемых выражений - я обращаюсь к тем, у кого есть опыт работы с этими макросами.

Ответ 1

Да, это разумно, и компиляторы могут и могут воспользоваться им в правильном сценарии.

В вашем фактическом примере, если could_be и very_improbable являются фактически интегральными переменными, не будет смысла вставлять макросы likely или unlikely в подвыражение предиката, потому что что может сделать компилятор, чтобы сделать это быстрее? Компилятор может организовать блок if по-разному в зависимости от вероятного результата ветки, но только потому, что very_improbably маловероятен, это не помогает: ему все еще нужно сгенерировать код для его проверки.

Возьмем пример, где компилятор может сделать больше работы:

extern int fn1();
extern int fn2();
extern int f(int x);

int test_likely(int a, int b) {
  if (likely(f(a)) && unlikely(f(b)))
    return fn1();
  return fn2();
}

Здесь предикат состоит из двух вызовов f() с аргументами, а icc создает другой код для 3 из 4 комбинаций likely и unlikely:

Код создан для likely(f(a)) && likely(f(b)):

test_likely(int, int):
        push      r15                                           #8.31
        mov       r15d, esi                                     #8.31
        call      f(int)                                         #9.7
        test      eax, eax                                      #9.7
        je        ..B1.7        # Prob 5%                       #9.7
        mov       edi, r15d                                     #9.23
        call      f(int)                                         #9.23
        test      eax, eax                                      #9.23
        je        ..B1.7        # Prob 5%                       #9.23
        pop       r15                                           #10.12
        jmp       fn1()                                       #10.12
..B1.7:                         # Preds ..B1.4 ..B1.2
        pop       r15                                           #11.10
        jmp       fn2()                                       #11.10

Здесь оба предиката, вероятно, верны, поэтому icc создает прямолинейный код для случая, когда оба являются истинными, выпрыгивая из строя, если либо оказывается ложным.

Код создан для unlikely(f(a)) && likely(f(b)):

test_likely(int, int):
        push      r15                                           #8.31
        mov       r15d, esi                                     #8.31
        call      f(int)                                         #9.7
        test      eax, eax                                      #9.7
        jne       ..B1.5        # Prob 5%                       #9.7
..B1.3:                         # Preds ..B1.6 ..B1.2
        pop       r15                                           #11.10
        jmp       fn2()                                       #11.10
..B1.5:                         # Preds ..B1.2
        mov       edi, r15d                                     #9.25
        call      f(int)                                         #9.25
        test      eax, eax                                      #9.25
        je        ..B1.3        # Prob 5%                       #9.25
        pop       r15                                           #10.12
        jmp       fn1()                                       #10.12

Теперь предикат, скорее всего, неверный, поэтому icc создает прямолинейный код, ведущий непосредственно к возврату в этом случае, и переходит из строки в B1.5, чтобы продолжить предикат. В этом случае он ожидает, что второй вызов (f(b)) будет истинным, поэтому он генерирует падение через код, заканчивающийся в хвост-вызов до fn1(). Если второй вызов оказывается ложным, он возвращается к той же последовательности, которая уже собрана для случая падения, хотя в первом прыжке (метка B1.3).

Это также является кодом, созданным для unlikely(f(a)) && unlikely(f(b)). В этом случае вы можете представить себе, что компилятор меняет конец кода, чтобы поставить jmp fn2() как провал, но это не так. Важно отметить, что это предотвратит повторное использование более ранней последовательности в B1.3, и также маловероятно, что мы даже выполняем этот код, поэтому представляется разумным предпочесть меньший размер кода для оптимизации уже маловероятного случая.

Код создан для likely(f(a)) && unlikely(f(b)):

test_likely(int, int):
        push      r15                                           #8.31
        mov       r15d, esi                                     #8.31
        call      f(int)                                         #9.7
        test      eax, eax                                      #9.7
        je        ..B1.5        # Prob 5%                       #9.7
        mov       edi, r15d                                     #9.23
        call      f(int)                                         #9.23
        test      eax, eax                                      #9.23
        jne       ..B1.7        # Prob 5%                       #9.23
..B1.5:                         # Preds ..B1.4 ..B1.2
        pop       r15                                           #11.10
        jmp       fn2()                                       #11.10
..B1.7:                         # Preds ..B1.4
        pop       r15                                           #10.12
        jmp       fn1()                                       #10.12

Это похоже на первый случай (likely && likely), за исключением того, что ожидание второго предиката теперь ложно, поэтому он переупорядочивает блоки так, что случай return fn2() является провальным.

Поэтому компиляторы определенно могут использовать точную информацию likely и unlikely, и действительно имеет смысл: если вы нарушили вышеуказанный тест на два прикомандированных оператора if, довольно очевидно, что будут работать отдельные подсказки для веток, поэтому неудивительно, что семантически эквивалентное использование && по-прежнему пользуется подсказками.

Вот несколько других заметок, которые не получили обработки "полного текста", если вы добрались до этого:

  • Я использовал icc, чтобы проиллюстрировать примеры, но для этого теста по крайней мере оба clang и gcc выполняют те же основные оптимизации (скомбинировав 3 из 4 случаев по-разному).
  • Одна "очевидная" оптимизация, которую мог бы сделать компилятор, зная вероятности суб-предикатов, заключается в том, чтобы отменить порядок предикатов. Например, если у вас есть likely(X) && unlikely(Y), вы можете сначала проверить условие Y, так как оно очень вероятно позволит вам проверять ярлык Y 1. По-видимому, gcc может сделать эту оптимизацию для простых предикатов, но я не смог уговорить icc или clang в это сделать. Оптимизация gcc, по-видимому, довольно хрупкая: исчезает, если вы немного измените предикат, хотя в этом случае оптимизация будет намного лучше.
  • Компиляторы не могут выполнять оптимизацию, если они не могут гарантировать, что преобразованный код будет вести себя "как бы", он был скомпилирован непосредственно в соответствии с семантикой языка. В частности, они имеют ограниченную возможность переупорядочивать операции, если они не могут доказать, что операции не имеют побочных эффектов. Помните об этом при структурировании ваших предикатов.

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

Ответ 2

Да, это может помочь. Например, в следующем примере, когда XXX выключен, GCC проверит x перед неожиданным y > 0, тем самым избегая выполнения маловероятного кода во время выполнения (Cygwin, gcc 5.4). Конечно, в этом конкретном случае проверка была проверена до x, но это не так важно, так как во время codegen ваш код может быть повторно перетасован GCC непредсказуемыми способами.

#ifdef XXX
#define __builtin_expect(x, y) (x)
#endif

void bar();

void foo(int x, int y, int z) {
  if(__builtin_expect(y > 0, 0) || x == 0)
    bar ();
}

Когда XXX выключен (т.е. __builtin_expect активен):

foo:
  testl   %ecx, %ecx
  je      .L4
  testl   %edx, %edx
  jg      .L4
  rep ret

Когда XXX определен (т.е. __builtin_expect игнорируется):

foo:
  testl   %edx, %edx
  jg      .L4
  testl   %ecx, %ecx
  je      .L4
  rep ret

Ответ 3

Да, C использует интеллектуальные или защитные функции. Поэтому, если мы пишем

  if(a() || b())

то a оценивается. Если true, b не оценивается. Если false, конечно, b необходимо оценить, чтобы принять окончательное решение.

Итак, если a() является дешевым или вероятным, b() дорогостоящим или невероятным, то сначала платит(). Но код эквивалентен

  if(a())
  {
      do_it();
  }
  /* action point */
  else if(b())
  {
     do_it();
  }

После того, как поток управления достигнет "action_point", вы увидите, что сообщение компилятору о том, может или нет, может помочь.