Почему gcc использует jmp для вызова функции в оптимизированной версии

#c #linux #gcc #x86

#c #linux #gcc #x86

Вопрос:

Когда я разобрал свою программу, я увидел, что gcc использует jmp для второго вызова pthread_wait_barrier при компиляции с -O3 . Почему это так?

Какое преимущество он получает, используя jmp вместо call. Какие трюки здесь использует компилятор? Я предполагаю, что здесь выполняется оптимизация хвостового вызова.

Кстати, я использую здесь статическое связывание.

 __attribute__ ((noinline)) void my_pthread_barrier_wait( 
    volatile int tid, pthread_barrier_t *pbar ) 
{
    pthread_barrier_wait( pbar );
    if ( tid == 0 )
    {
        if ( !rollbacked )
        {
            take_checkpoint_or_rollback(   iter == 4 );
        }
    }
    //getcontext( amp;context[tid] );
    SETJMP( tid );
    asm("addr2jmp:"); 
    pthread_barrier_wait( pbar );
    // My suspicion was right, gcc was performing tail call optimization, 
    // which was messing up with my SETJMP/LONGJMP implementation, so here I
    // put a dummy function to avoid that.
    dummy_var = dummy_func();
}
  

Комментарии:

1. Покажите нам исходный код.

2. jmp просто переносит выполнение в новое местоположение. вызов помещает материал в стек и, следовательно, немного дороже в использовании.

3. Похоже, что он выполняет оптимизацию хвостового вызова.

4. Можете ли вы добавить код на ассемблере?

Ответ №1:

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

 return func2(...)
  

или вообще не имеет возвращаемого типа ( void ) .

В этом случае «мы» оставляем «наш» адрес возврата в стеке, оставляя его «им», чтобы использовать его для возврата к «нашему» вызывающему.

Ответ №2:

Возможно, это был хвостовой рекурсивный вызов. У GCC есть некоторый проход, выполняющий хвостовую рекурсивную оптимизацию.

Но зачем вам беспокоиться? Если вызываемая функция является extern функцией, то она общедоступна, и GCC должен вызывать ее в соответствии с соглашениями ABI (что означает, что она соответствует соглашению о вызове).

Вас не должно волновать, была ли функция вызвана jmp.

И это также может быть вызов динамической библиотечной функции (т. Е. С PLT для динамического связывания)

Ответ №3:

у jmp меньше накладных расходов, чем у call. jmp просто прыгает, call помещает некоторые данные в стек и прыгает

Комментарии:

1. -1 за неполный ответ. Я также знаю, что у jmp меньше накладных расходов. Вопрос заключался в том, как gcc использует jmp для выполнения тех же функций, что и call в оптимизированной версии.

2. То, что вы упоминаете в своем комментарии, четко не указано в вопросе, возможно, вам следует его отредактировать. Единственное утверждение вопроса, на которое я не ответил, было «Какие трюки здесь использует компилятор?», Что является неясным и ломаным английским.

3. Конечно, у JMP меньше накладных расходов. Теперь функция, к которой вы переходите, а не вызываете, должна вернуться в какой-то момент. Следовательно, адрес возврата должен быть помещен в стек перед JMP или в конце функции вам нужно вызвать другой JMP, чтобы вернуться туда, откуда вы пришли, что, в целом, в конечном итоге очень похоже. Вы можете сэкономить пару циклов с помощью двойного JMP, поскольку нет манипуляций со стеком, но вам придется хранить обратный адрес в регистре или что-то в этом роде.

Ответ №4:

Я предполагаю, что это конечный вызов, то есть либо текущая функция возвращает результат вызываемой функции без изменений, либо (для функции, которая возвращает void) возвращает сразу после вызова функции. В любом случае нет необходимости использовать call .

call Инструкция выполняет две функции. Во-первых, он помещает адрес инструкции после вызова в стек в качестве обратного адреса. Затем он переходит к месту назначения вызова. ret извлекает адрес возврата из стека и переходит в это местоположение.

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

Ответ №5:

Вы никогда не узнаете, но одной из вероятных причин является «кэширование» (среди других причин, таких как уже упомянутая оптимизация хвостовых вызовов).

Встраивание может ускорить и замедлить код, потому что чем больше кода, тем меньше его будет в кэше L1 за один раз.

JMP позволяет компилятору повторно использовать один и тот же фрагмент кода практически без затрат. Современные процессоры имеют глубокую конвейерную обработку, и конвейеры проходят через JMP без проблем (здесь нет возможности неправильного предсказания!). В среднем случае это будет стоить всего 1-2 цикла, в лучшем случае нулевых циклов, потому что процессору все равно придется ждать предыдущей инструкции, чтобы завершить работу. Это, очевидно, полностью зависит от соответствующего индивидуального кода.
В принципе, компилятор мог бы сделать это даже с несколькими функциями, которые имеют общие части.