2016-05-18 2 views
4

Когда мы скомпилируем код и выполним его, в сборке, в которой наш код преобразуется, функции хранятся не последовательно. Поэтому каждый раз, когда вызывается функция, процессор должен отбрасывать инструкции в конвейере. Не влияет ли это на производительность программы?Функции в нашем коде делают это медленнее?

PS: Я не рассматриваю время, потраченное на разработку таких программ без функций. Чисто на уровне производительности. Есть ли способы, с помощью которых компиляторы справляются с этим, чтобы уменьшить его?

+0

Обычно прыжки на функции и обратно не проблема, в то время как прыжки на условиях («ветви») ... как каждый раз, когда вы используете if/while/for/etc ... вот довольно хорошая публикация об этом : http://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster-than-an-unsorted-array/11227902#11227902 – Tommylee2k

+0

В современных процессорах неверное предсказание отрасли часто бывает дешевле, чем кэш промахов. В то время как компилятор может предотвращать появление браков (и, следовательно, неверное предсказание) с помощью встроенных функций и циклов разворота, это значительно увеличивает размер кода. Раньше существовали способы помочь предиктору отрасли, [они больше не работают] (http://stackoverflow.com/a/1851905). –

+0

Передний конец должен быть перенаправлен в то время, прежде чем задний конец заметит что-либо, но, конечно, если есть недостаток в кеше из-за нелокальности, этот механизм немного побежден. С другой стороны, вложение много приводит к промахам пропускной способности, поэтому есть компромисс. – harold

ответ

7

Таким образом, каждый раз, когда вызывается функция, процессор должен отбрасывать инструкции в конвейере.

Нет, все после этапа декодирования все еще хорошо. ЦП знает, что не следует продолжать декодирование после безусловной ветки (например, jmp, call или ret). Только команды, которые были извлечены, но еще не декодированы, - это те, которые не должны выполняться. Пока целевой адрес не будет декодирован из инструкции, нет ничего полезного для начала выполнения конвейера, поэтому вы получаете пузыри в конвейере до тех пор, пока целевой адрес не будет известен. Инструкции по расшифровке разделов как можно раньше минимизируют штраф за принимаемые филиалы.

В classic RISC pipeline, этапы являются IF ID EX MEM WB (выборка, декодирование, выполнение, мем, обратной записи (результаты в регистрах). Поэтому, когда ID декодирует команду ветвления, трубопровод отбрасывает команду в настоящее время выбираются в IF, и инструкция, которая в настоящее время декодируется в ID (потому что это инструкция после ветки).

«Опасность» - это термин, который препятствует постоянному потоку инструкций проходить по трубопроводу за один раз за часы. Control Hazard. (Управление, как при управлении потоком, в отличие от данных.)

Если цель ветвления не находится в I-кеше L1, трубопровод wi придется дождаться, когда инструкции будут транслироваться из памяти до того, как этап IF-конвейера может вывести извлеченную команду. I-cache misses всегда создают пузырь трубопровода. Предварительная выборка обычно избегает этого для не ветвящегося кода.


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

Кроме того, вместо фактического декодирования для обнаружения инструкций ветвления ЦПУ может проверять каждый адрес команды на кеш "Branch Target Buffer". Если вы получаете удар, вы знаете, что команда является веткой, хотя вы еще не расшифровали ее. BTB также содержит целевой адрес, поэтому вы можете сразу начать выборку (если это безусловная ветвь или ваш процессор поддерживает спекулятивное выполнение на основе прогноза ветвления).


ret на самом деле сложнее случай: обратный адрес в регистре или в стеке, не закодированную непосредственно в инструкцию. Это безусловный непрямой филиал. Современные процессоры x86 поддерживают внутренний стек предсказателей обратного адреса и очень плохо работают, когда вы неправильно соглашаетесь на команды вызова/ret. Например. call label/label: pop ebx ужасно подходит для независимого от позиции 32-битного кода, чтобы получить EIP в EBX. Это вызовет неверное предсказание в течение следующих 15 или около ret s вверх по дереву вызовов.

Я думаю, что я читал, что a return-address predictor stack используется некоторыми другими микроархитектурами, отличными от x86.

См Agner Fog's microarchitecture pdf, чтобы узнать больше о том, как x86 процессорах ведут себя (также см тегов вика), или читать компьютерную архитектуру учебник, чтобы узнать о simple RISC pipelines.

Подробнее о кэшах и памяти (в основном ориентированных на кэширование/предварительную выборку данных), см. Ulrich Drepper's What Every Programmer Should Know About Memory.


Безусловная ветка довольно дешевая, как правило, несколько циклов в худшем случае (не считая промахов I-cache).

Большая стоимость вызова функции заключается в том, когда компилятор не может видеть определение целевой функции и должен предположить, что он захватывает все регистры, сбрасываемые вызовом, в вызывающем соглашении. (В x86-64 SystemV, все регистры float/vector и около 8 целых регистров.) Это требует либо проливания памяти, либо сохранения живых данных в регистры, сохраненные в кодах. Но это означает, что функция должна сохранять/восстанавливать этот регистр, чтобы не сломать вызывающего абонента.

Межпроцессная оптимизация, позволяющая функциям использовать преимущества того, что регистрирует другие функции, фактически сбивающиеся, а какие нет, - это то, что компиляторы могут делать в одном модуле компиляции. Или даже через компиляторы с оптимизацией всей программы link-time. Но он не может распространяться на границы динамической привязки, потому что компилятору не разрешается создавать код, который будет разбиваться с иначе скомпилированной версией той же общей библиотеки.


Существуют ли какие-либо способы, в которых Составители дело с этим, чтобы уменьшить его?

Они встроенные небольшие функции или даже большие static функции, которые вызывается только один раз.

e.g.

int foo(void) { return 1; } 
    mov  eax, 1 #, 
    ret 

int bar(int x) { return foo() + x;} 
    lea  eax, [rdi+1]  # D.2839, 
    ret 

Как @harold указывает, переусердствовать с встраивание может привести к кэш-промахов, тоже, потому что он раздувает ваш размер кода, так много, что не все ваши горячей код помещается в кэш.

Проекты Intel SnB-семейства имеют небольшой, но очень быстрый кеш-кэш, который кэширует декодированные инструкции. Он содержит не более 1536 uops IIRC, в строках по 6 uops каждый. Выполнение из кэша uop вместо декодеров сокращает штраф за ветвление с неверным прогнозом от 19 до 15 циклов, IIRC (что-то вроде этого, но эти числа, вероятно, не совсем корректны для какого-либо конкретного uarch). Существует также значительное увеличение пропускной способности интерфейса по сравнению с декодерами, особенно. для длинных инструкций, которые являются общими в векторном коде.

Смежные вопросы