2017-01-10 2 views
1

Когда дело доходит до временного хранения существующего значения в регистре, все современные компиляторы (по крайней мере, те, что я испытал) выполняют инструкции PUSH и POP. Но почему бы не сохранить данные в другом регистре, если они доступны?"PUSH" "POP" Или "MOVE"?

Итак, где должно временно храниться существующее значение? Stack Or Register?

Рассмотрим следующий код: 1-ый

MOV ECX,16 
LOOP: 
PUSH ECX ;Value saved to stack  
...  ;Assume that here's some code that must uses ECX register 
POP ECX  ;Value released from stack 
SUB ECX,1 
JNZ LOOP 

Теперь рассмотрим 2ст код:

MOV ECX,16 
LOOP: 
MOV ESI,ECX ;Value saved to ESI register  
...  ;Assume that here's some code that must uses ECX register 
MOV ECX,ESI ;Value returned to ECX register 
SUB ECX,1 
JNZ LOOP 

В конце концов, что один из кода выше, лучше и почему?

Лично я думаю, что первый код лучше по размеру, так как PUSH и POP занимает всего 1 байт, а MOV - 2; и второй код лучше по скорости, потому что данные, перемещающиеся между регистрами, быстрее, чем доступ к памяти.

+5

Вы используете значения «push» в стеке, чтобы вы могли использовать регистры, которые они занимают. Почему бы им не переместить их в другие регистры? Вероятно, потому что другие регистры нужны и для некоторых значений. – fuz

+2

Если регистр ESI свободен в цикле, вам будет лучше поставить счетчик в ESI и просто не перетасовать его. Если ваш компилятор умный, он это узнает. Вывод: либо у вас есть немой компилятор, либо он знает, что ESI также не свободен в цикле, и нет других свободных регистров. В этом случае комбинация PUSH/POP не страшна. –

+0

* «все современные компиляторы (по крайней мере, те, что я испытал) выполняют инструкции PUSH и POP» * ... это довольно фиктивное утверждение, попробуйте «gcc» или «clang», они этого не делают (если они не закончились резервных регистров внутри цикла, то я бы поспорил, что они предпочитают использовать локальную переменную '[ebp-ofs// esp + ofs]. Мне бы хотелось увидеть, что некоторые источники C/C++ создают PUSH/POP с этими двумя. И снова эти два являются в основном единственными современными компиляторами, поэтому я не уверен, что вы проверили. – Ped7g

ответ

1

Использование регистра выполняется немного быстрее, но вам необходимо следить за тем, какие регистры доступны, и вы можете выходить из регистров. Кроме того, этот метод нельзя использовать рекурсивно. Кроме того, некоторые регистры будут разбиты, если вы используете INT или CALL для вызова подпрограммы.

Использование стека (POP и PUSH) может использоваться столько раз, сколько необходимо (до тех пор, пока вы не закончите пространство стека), и, кроме того, он поддерживает рекурсивную логику. Вы можете безопасно использовать стек с INT или CALL, потому что по соглашению любая подпрограмма должна зарезервировать свою часть стека и должна восстановить ее до своего предыдущего состояния (иначе команда RET завершится с ошибкой).

1

Когда вы думаете о скорости, вы всегда должны иметь в виду чувство меры.

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

Писатели-компиляторы знают, что в этом случае, который очень распространен, не должно быть penny-wise and pound-foolish.

1

Используя PUSH и POP, вы можете сохранить как минимум один регистр. Это будет значительным, если вы работаете с ограниченными доступными реестрами. С другой стороны, да, иногда использование MOV лучше в скорости, но вы также должны иметь в виду, какой регистр используется в качестве временного хранилища. Это будет сложно, если вы хотите сохранить несколько значений, которые необходимо было обработать позже.

1

Это действительно имеет смысл. Но я думаю, что самый простой ответ - все остальные регистры используются. Чтобы использовать какой-либо другой регистр, вам нужно нажать его в стек.

Компиляторы достаточно умны. Отслеживание того, что находится в регистре для компилятора, несколько тривиально, это не проблема. Говоря в общем случае не обязательно x86, особенно если у вас больше регистров (чем у x86), у вас появятся некоторые регистры, которые используются для ввода (в вашем соглашении о вызове), некоторые из которых вы можете уничтожить, это может быть то же самое, что и входные или нет, некоторые из которых вы не можете мусор, вы должны сохранить их в первую очередь. У некоторых наборов инструкций есть специальные регистры, они должны использовать этот для автоматического приращения, один для регистрации непрямой и т. Д.

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

unsigned int more_fun (unsigned int); 
unsigned int fun (unsigned int x) 
{ 
    return(more_fun(x)+x); 
} 
00000000 <fun>: 
    0: e92d4010 push {r4, lr} 
    4: e1a04000 mov r4, r0 
    8: ebfffffe bl 0 <more_fun> 
    c: e0840000 add r0, r4, r0 
    10: e8bd4010 pop {r4, lr} 
    14: e12fff1e bx lr 

Я сказал вам, что это тривиально. Теперь, чтобы использовать ваш аргумент назад, почему они просто не нажимали r0 на стек и не выскакивали позже, зачем нажать r4? Для входа используются не r0-r3, а волатильны, r0 - регистр возврата, когда он подходит, r4 почти все, что вам нужно сохранить (за исключением одного я думаю).

Таким образом, предполагается, что r4 используется вызывающим абонентом или некоторым вызывающим абонентом вверх по линии, вызывающая конвенция диктует, что вы не можете ее уничтожить, вы должны ее сохранить, поэтому вы должны предположить, что она используется. Вы можете уничтожить r0-r3, но вы не можете использовать один из них, так как вызывающий может их уничтожить, поэтому в этом случае нам нужно принять входящее значение x и использовать его (передать его) и сохранить его после возвращения поэтому они сделали оба, «использовали другой регистр с ходом», но для этого они сохранили этот другой регистр.

Зачем сохранять r4 в стеке в этом случае очень очевидно, вы можете сохранить его спереди с обратным адресом, в частности, рука хочет, чтобы вы всегда использовали стек в 64-битных кусках, поэтому два регистра за раз идеально или по крайней мере, держите его в соответствии с 64-битной границей, поэтому вам все равно нужно сохранить lr, поэтому они будут продвигать что-то еще, даже если они этого не делают, поскольку в этом случае сохранение r4 является халявой, и поскольку им нужно для сохранения r0 и в то же время использовать его. r4 или r5 или что-то выше - хороший выбор.

BTW выглядит как компилятор x86, выполненный выше.

0000000000000000 <fun>: 
    0: 53      push %rbx 
    1: 89 fb     mov %edi,%ebx 
    3: e8 00 00 00 00   callq 8 <fun+0x8> 
    8: 01 d8     add %ebx,%eax 
    a: 5b      pop %rbx 
    b: c3      retq 

демонстрация их толкает что-то, что они не должны сохранить:

unsigned int more_fun (unsigned int); 
unsigned int fun (unsigned int x) 
{ 
    return(more_fun(x)+1); 
} 
00000000 <fun>: 
    0: e92d4010 push {r4, lr} 
    4: ebfffffe bl 0 <more_fun> 
    8: e8bd4010 pop {r4, lr} 
    c: e2800001 add r0, r0, #1 
    10: e12fff1e bx lr 

Нет причины, чтобы спасти r4, они просто необходимы некоторые регистр, чтобы стек выравнивается, так что в этом случае r4 был выбран , в некоторых версиях этого компилятора вы увидите r3 или какой-либо другой регистр.

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

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

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

unsigned int fun (unsigned int x, unsigned int y, unsigned int z) 
{ 
    unsigned int a; 

    a=x<<y; 
    a+=(y<<z); 
    a+=x+y+z; 
    return(a); 
} 
00000000 <fun>: 
    0: e0813002 add r3, r1, r2 
    4: e0833000 add r3, r3, r0 
    8: e0832211 add r2, r3, r1, lsl r2 
    c: e0820110 add r0, r2, r0, lsl r1 
    10: e12fff1e bx lr 

Только потратил на это несколько секунд, но мог бы усердно работать над этим. Я не пропустил всего четыре регистра, и у меня было четыре переменные. И я не называл какие-либо функции, поэтому компилятор был свободен, чтобы просто мусорить r0-r3 по мере необходимости в зависимости от разработанных зависимостей. Поэтому мне не нужно было сохранять r4 для создания временного хранилища, ему не нужно было использовать стек, он просто оптимизировал порядок выполнения, например, освободить r2, переменную z, чтобы позже он мог использовать r2 в качестве промежуточной переменной , один из примеров чего-то равного. Хранение его до четырех регистров вместо того, чтобы сжечь пятый.

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

Я сказал это семь раз, но нижняя строка - это соглашение о вызове для этого компилятора (версии) и цели (и параметров командной строки/значений по умолчанию). Если у вас есть летучие регистры (произвольная информация о соглашениях общего пользования для регистров общего назначения, а не аппаратная/ISA-вещь), и вы не вызываете каких-либо других функций, тогда они просты в использовании и сохраняют дорогостоящие транзакции стека (памяти). Если вы звоните кому-то, тогда они могут быть разбиты ими, поэтому они могут больше не быть свободными, зависит от вашего кода. Энергонезависимые регистры считаются потребляемыми абонентами, поэтому вам приходится записывать операции стека, чтобы использовать их, они не могут свободно использоваться. И тогда это становится производительностью относительно того, когда и где использовать стек, толкает и всплывает, и движется. Ожидается, что два компилятора не сгенерируют один и тот же код, даже если они используют одно и то же соглашение, но вы можете видеть выше, несколько тривиально, чтобы выполнять тестовые функции, компилировать их и анализировать выходные данные, настраивать здесь и там, чтобы перемещаться по ним и вокруг него (компилятор, версия и целевые и условные и параметры командной строки).

+0

«Помните, что люди пишут оптимизаторы»: правильно. Поскольку они делают это с помощью алгоритмов (таких как распределители регистров), которые ищут оптимальные конфигурации, возможно, даже сами они не могут предсказать результаты. –

+0

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

+0

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

0

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

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

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

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