2

Рассмотрим ситуацию, когда мы пишем код C. Когда компилятор встречает вызов функции, я понимаю, что он делает следующее:Использование регистра блокировки для определенного раздела кода

  • Нажми всех регистров в стек
  • Перейти к новой функции, делать вещи там
  • Pop старого контекста выключены стек обратно в регистры.

Теперь у некоторых процессоров есть 1 рабочий регистр, около 32, что-то еще. Меня больше всего интересует большее количество регистров. Если у моего процессора 32 регистра, компилятор должен будет выпустить 32 команды push и pop, а также базовые служебные данные для вызова функции. Было бы неплохо, если бы я мог обменять гибкость компиляции [1] в функции для меньших команд push и pop. То есть, я хотел бы так, что я мог бы сказать компилятору «Для функции foo() использовать только 4 регистра. Это будет означать, что компилятор будет необходимо всего лишь нажать/поп 4 регистра перед прыжком в foo().

Я понимаю, что это довольно глупо беспокоиться на современном ПК, но я больше думаю о низкоскоростной встроенной системе, где вы можете очень быстро обслуживать прерывание или многократно использовать простую функцию. Я также понимаю, что это может очень быстро становятся компонентом, зависящим от архитектуры. Процессоры, использующие набор инструкций «Source Source -> Dest» (как ARM), в отличие от аккумулятора (например, Freescale/NXP HC08) могут иметь некоторое нижнее ограничение на количество регистров, которые мы разрешаем функции для использования.

Я знаю, что компилятор использует трюки, такие как создание небольших функций для увеличения скорости, и я понимаю, что могу сообщить большинству компиляторов не генерировать код push/pop и просто сам его код в сборке, но мой вопрос фокусируется на инструктировании компилятора сделайте это от «C-Land».

Мой вопрос: есть ли компиляторы, которые разрешают это? Это даже необходимо при оптимизации компиляторов (они уже делают это)?

[1] Гибкость компиляции: уменьшая количество доступных для компилятора регистров, которые вы используете в теле функции, вы ограничиваете ее гибкость, и, возможно, потребуется больше использовать стек, поскольку он не может просто использовать другой регистр.

+5

Ваше понимание неверно. Нет причин подталкивать все регистры, и они, конечно же, этого не делают. Некоторые регистры являются входами, некоторые выходы и некоторые из них никогда не используются, поэтому нет причин для их сохранения. Просмотрите соглашения о вызовах, и вы увидите, как это делается. Компилятор, безусловно, знает, какие регистры он использует, и может сохранить свое состояние, если это необходимо. –

+0

@SamiKuhmonen О, хорошо, спасибо. – bss36504

+1

Слишком широкий. Это включает в себя CPU, API, оптимизацию компилятора, операционную систему/библиотеку, компоновщик и т. Д. Если вам требуется конкретное использование регистров, вам также может понадобиться определенная структура кода, что означает Assembler (возможно, встроенный). Также некоторый компилятор уже позволяет зарезервировать глобальные регистры, но часто показывает, что это хуже, чем принятие некоторых других инструкций сохранения/восстановления в коде. И это от MCU-view, а не (просто) больших утюгов. – Olaf

ответ

2

Когда речь идет о компиляторах, регистрах и функциональных вызовах, вы можете, как правило, думать о том, что регистры попадают в одну из трех категорий: «руки», неустойчивые и нестабильные.

Категория «руки в выключенном состоянии» - это те, которые компилятор обычно не использует, если вы явно не указали это (например, с встроенной сборкой). Они могут включать регистры отладки и другие регистры специального назначения. Список будет варьироваться от платформы к платформе.

Неустойчивый (или скремблированный/сбрасываемый/вызываемый звонком) набор регистров - это те, с которыми функция может работать без необходимости сохранения. То есть, вызывающий абонент понимает, что содержимое этих регистров может не совпадать после вызова функции. Таким образом, если у вызывающего есть какие-либо данные в тех регистрах, которые он хочет сохранить, он должен сохранить эти данные перед выполнением вызова, а затем восстановить его после. На 32-разрядной платформе x86 эти изменчивые регистры (иногда называемые регистры царапин) обычно являются EAX, ECX и EDX.

Энергонезависимый (или сохраненный или сохраненный пользователем) набор регистров - это те, которые функция должна сохранять перед использованием и восстановить их исходные значения перед возвратом.Они должны быть сохранены/восстановлены вызываемой функцией, если она их использует. На 32-разрядной платформе x86 обычно это остальные регистры общего назначения: EBX, ESI, EDI, ESP, EBP.

Надеюсь, это поможет.


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

Для более конкретного примера SysV x86-64 ABI хорошо спроектирован (с аргументами, переданными в регистрах, и хороший баланс по-разному - по отношению к царапинам/arg regs). Есть и другие ссылки в вики-файле , объясняющие, какие соглашения об ABI/вызовах существуют.

Рассмотрим простой пример с вызовами функции, которые не могут быть встраиваемыми (потому что определение не доступен):

int foo(int); 

int bar(int a) { 
    return 5 * foo(a+2) + foo (a) ; 
} 

It compiles (on godbolt with gcc 5.3 for x86-64 with -O3 к следующему:

## gcc output 
    # AMD64 SysV ABI: first arg in e/rdi, return value in e/rax 
    # the call-preserved regs used are: rbp and rbx 
    # the scratch regs used are: rdx. (arg-passing/return regs are not call-preserved) 
    push rbp    # save a call-preserved reg 
    mov  ebp, edi  # stash `a` in a call-preserved reg 
    push rbx    # save another call-preserved reg 
    lea  edi, [rdi+2] # edi=a+2 as an arg for foo. `add edi, 2` would also work, but they're both 3 bytes and little perf difference 
    sub  rsp, 8   # align the stack to a 16B boundary (the two pushes are 8B each, and call pushes an 8B return address, so another 8B is needed) 
    call foo    # eax=foo(a+2) 
    mov  edi, ebp  # edi=a as an arg for foo 
    mov  ebx, eax  # stash foo(a+2) in ebx 
    call foo    # eax=foo(a) 
    lea  edx, [rbx+rbx*4] # edx = 5*foo(a+2), using the call-preserved register 
    add  rsp, 8   # undo the stack offset 
    add  eax, edx  # the add between the to function-call results 

    pop  rbx    # restore the call-preserved regs we saved earlier 
    pop  rbp 
    ret      # return value in eax 

Как обычно, компиляторы могли бы сделать лучше: вместо того, чтобы спрятать foo(a+2) в ebx, чтобы выжить во втором звонке до foo, он мог бы спрятать 5*foo(a+2) с одной инструкцией (lea ebx, [rax+rax*4]). Кроме того, необходим только один регистр с сохранением вызова, поскольку нам не нужно a после второго call. Это удаляет пару push/pop, а также пару sub rsp,8/add rsp,8. (gcc bug report already filed for this missed optimization)

## Hand-optimized implementation (still ABI-compliant): 
    push rbx    # save a call-preserved reg; also aligns the stack 

    lea  ebx, [rdi+2] # stash ebx=a+2 
    call foo    # eax=foo(a) 
    mov  edi, ebx  # edi=a+2 as an arg for foo 
    mov  ebx, eax  # stash foo(a) in ebx, replacing `a+2` which we don't need anymore 
    call foo    # eax=foo(a+2) 
    lea  eax, [rax+rax*4] #eax=5*foo(a+2) 
    add  eax, ebx  # eax=5*foo(a+2) + foo(a) 

    pop  rbx    # restore the call-preserved regs we saved earlier 
    ret      # return value in eax 

Обратите внимание, что вызов foo(a) происходит перед foo(a+2) в этой версии. Он сохранил инструкцию с самого начала (так как мы можем передать наш аргумент без изменений до первого вызова foo), но позже удалили потенциальную экономию (поскольку умножение на 5 теперь должно произойти после второго вызова и может " t быть объединенным с перемещением в регистр, сохраняемый вызовом).

Я мог бы избавиться от дополнительного mov, если это было 5*foo(a) + foo(a+2). С выражением, которое я написал, я не могу комбинировать арифметику с перемещением данных (используя lea) в каждом случае. Или мне нужно будет сэкономить a и сделать отдельный add edi,2 перед первым call.

+0

Регистры 'volatile' на самом деле бесполезны, поскольку регистры обычно являются« локальными/временными переменными ». OP говорит о регистрах CPU, а не о периферийных регистрах. Обратите внимание, что ваш пример не является тем, что «volatile» в C означает. Регистр нуля - временная (неназванная) переменная. – Olaf

+0

@Olaf: Хорошее наблюдение за использованием слова «volatile». Кроме того, регистры нуля, как вы указываете, не совсем то же самое, что и волатильные регистры; однако, поскольку волатильные регистры часто (но не исключительно) используются для временного хранения значения, они по-прежнему иногда упоминаются как регистры нуля и далеки от бесполезности. – Sparky

+0

Пожалуйста, не используйте термин «volatile» в контексте C, поскольку вы используете его в других контекстах. Последствия ** очень разные. Вы в основном различаете локальные и глобальные регистры, как я пытался указать, что не имеет ничего общего с 'volatile', и нельзя использовать термин в C в любом другом контексте. Глобальные регистры, которые вы перечисляете для x86, на самом деле совпадают с вашей первой категорией: руки. – Olaf

1

Нажмите все регистры в стек

Нет, в подавляющем большинстве вызовов функций в оптимизированном коде, лишь малая часть всех регистров в стек.

В основном я интересуюсь большим количеством регистров.

Есть ли у вас какие-либо экспериментальные данные для поддержки этой проблемы? Это узкое место в производительности?

Я мог бы продать некоторую гибкость компиляции [1] в функции для менее push и pop инструкции.

Современные компиляторы используют сложное межпроцедурное распределение регистров. Ограничивая количество регистров, вы, скорее всего, ухудшите производительность.

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

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