Используют ли языки/компиляторы инструкцию x86 ENTER с ненулевым уровнем вложенности?
Те, кто знаком с программированием сборки x86, очень привыкли к типичной функции пролог/эпилог:
push ebp
mov esp, ebp
sub esp, [size of local variables]
...
mov esp, ebp
pop ebp
ret
Эта же последовательность кода также может быть реализована с помощью инструкций ENTER
и LEAVE
:
enter [size of local variables], 0
...
leave
ret
Второй операнд ENTER
инструкции - это уровень вложенности, который позволяет получить доступ к нескольким родительским кадрам из вызываемой функции.
Это не используется в C, потому что нет вложенных функций; локальные переменные имеют только область действия функции, в которой они объявлены. Эта конструкция не существует (хотя иногда мне это хотелось):
void func_a(void)
{
int a1 = 7;
void func_b(void)
{
printf("a1 = %d\n", a1); /* a1 inherited from func_a() */
}
func_b();
}
Однако у Python есть вложенные функции, которые ведут себя следующим образом:
def func_a():
a1 = 7
def func_b():
print 'a1 = %d' % a1 # a1 inherited from func_a()
func_b()
Конечно, код Python не переводится непосредственно на машинный код x86 и, следовательно, не сможет (вряд ли?) воспользоваться этой инструкцией.
Существуют ли какие-либо языки, которые компилируются на x86 и предоставляют вложенные функции? Существуют ли компиляторы, которые будут генерировать инструкцию ENTER
с ненулевым вторым операндом?
Intel инвестировала ненулевое количество времени/денег в этот операнд уровня вложенности, и в основном мне просто интересно, если кто-нибудь его использует: -)
Литература:
Ответы
Ответ 1
enter
на практике избегается, так как он работает довольно плохо - см. ответы "enter" vs "push ebp; mov ebp, esp; sub esp, imm" и "отпуск" vs "mov esp, ebp; pop ebp" . Есть куча инструкций x86, которые устарели, но по-прежнему поддерживаются по соображениям обратной совместимости. enter
- один из них. (leave
ОК, хотя компиляторы с удовольствием испускают его.)
Реализация вложенных функций в полной общности, как и в Python, на самом деле представляет собой значительно более интересную проблему, чем просто выбор нескольких команд управления кадрами - поиск "преобразования закрытия" и "проблемы с восходящей/нисходящей потерей", и вы найдете много интересных обсуждений.
Обратите внимание, что x86 изначально был спроектирован как машина Pascal, поэтому есть инструкции для поддержки вложенных функций (enter
, leave
), соглашения о вызове pascal
, в котором вызываемая группа выдает известное количество аргументы из стека (ret K
), проверка границ (bound
) и т.д. Многие из этих операций теперь устарели.
Ответ 2
Как Iwillnotexist Idonotexist отметил, GCC поддерживает вложенные функции в C, используя точный синтаксис, который я показал выше.
Однако он не использует инструкцию ENTER
. Вместо этого переменные, которые используются во вложенных функциях, группируются вместе в области локальных переменных, а указатель на эту группу передается вложенную функцию. Интересно, что этот "указатель на родительские переменные" передается через нестандартный механизм: на x64 он передается в r10
, а на x86 (cdecl) он передается в ecx
, который зарезервирован для указателя this
в С++ (который в любом случае не поддерживает вложенные функции).
#include <stdio.h>
void func_a(void)
{
int a1 = 0x1001;
int a2=2, a3=3, a4=4;
int a5 = 0x1005;
void func_b(int p1, int p2)
{
/* Use variables from func_a() */
printf("a1=%d a5=%d\n", a1, a5);
}
func_b(1, 2);
}
int main(void)
{
func_a();
return 0;
}
Производит следующий (фрагмент кода) при компиляции для 64-разрядного:
00000000004004dc <func_b.2172>:
4004dc: push rbp
4004dd: mov rbp,rsp
4004e0: sub rsp,0x10
4004e4: mov DWORD PTR [rbp-0x4],edi
4004e7: mov DWORD PTR [rbp-0x8],esi
4004ea: mov rax,r10 ; ptr to calling function "shared" vars
4004ed: mov ecx,DWORD PTR [rax+0x4]
4004f0: mov eax,DWORD PTR [rax]
4004f2: mov edx,eax
4004f4: mov esi,ecx
4004f6: mov edi,0x400610
4004fb: mov eax,0x0
400500: call 4003b0 <[email protected]>
400505: leave
400506: ret
0000000000400507 <func_a>:
400507: push rbp
400508: mov rbp,rsp
40050b: sub rsp,0x20
40050f: mov DWORD PTR [rbp-0x1c],0x1001
400516: mov DWORD PTR [rbp-0x4],0x2
40051d: mov DWORD PTR [rbp-0x8],0x3
400524: mov DWORD PTR [rbp-0xc],0x4
40052b: mov DWORD PTR [rbp-0x20],0x1005
400532: lea rax,[rbp-0x20] ; Pass a, b to the nested function
400536: mov r10,rax ; in r10 !
400539: mov esi,0x2
40053e: mov edi,0x1
400543: call 4004dc <func_b.2172>
400548: leave
400549: ret
Выход из objdump --no-show-raw-insn -d -Mintel
Это будет эквивалентно чему-то более подробному:
struct func_a_ctx
{
int a1, a5;
};
void func_b(struct func_a_ctx *ctx, int p1, int p2)
{
/* Use variables from func_a() */
printf("a1=%d a5=%d\n", ctx->a1, ctx->a5);
}
void func_a(void)
{
int a2=2, a3=3, a4=4;
struct func_a_ctx ctx = {
.a1 = 0x1001,
.a5 = 0x1005,
};
func_b(&ctx, 1, 2);
}
Ответ 3
Наш PARLANSE компилятор (для мелкозернистых параллельных программ на SMP x86) имеет лексическое определение.
PARLANSE пытается сгенерировать много, много небольших параллельных зерен вычисления, а затем мультиплексирует их поверх потоков (1 на процессор). Фактически, фреймы стека выделены в кучу; мы не хотели платить цену за "большой стек" за каждое зерно, так как у нас их много, и мы не хотели ограничивать то, как глубоко можно что-либо повторить. Из-за параллельных вилок стек фактически представляет собой стек кактуса.
Каждая процедура, при входе, создает лексический дисплей, чтобы разрешить доступ к окружающим лексическим областям. Мы рассмотрели использование инструкции ENTER, но решили против нее по двум причинам:
- Как отмечали другие, это не особенно быстро. Инструкции MOV так же хорошо.
- Мы заметили, что дисплей часто разрежен и имеет тенденцию быть более плотным на лексически более глубокой стороне. Большинство внутренних вспомогательных функций отлично справляются с доступом только к их непосредственному лексическому родительскому элементу; вам не всегда нужен доступ ко всем вашим родителям. Иногда нет.
Следовательно, компилятор точно определяет, какие лексические области, к которым функция нуждается в доступе, и генерирует в пролог функции, куда должен идти ЕГЭ, только инструкции MOV, чтобы скопировать часть родительского отображения, которое действительно необходимо. Это часто оказывается 1 или 2 парами ходов.
Итак, мы выигрываем дважды по производительности, используя ENTER.
IMHO, ENTER теперь является одной из тех устаревших инструкций CISC, которая казалась хорошей идеей в то время, когда она была определена, но превзойдены последовательностями команд RISC, которые оптимизирует даже Intel x86.
Ответ 4
Я сделал некоторую статистику подсчета команд для загрузки Linux с использованием виртуальной платформы Simics и обнаружил, что ВВОД никогда не использовался. Однако в миксе было довольно много инструкций LEAVE. Между CALL и LEAVE была почти 1-1 корреляция. Казалось бы, это подтверждает идею о том, что ENTER просто медленный и дорогой, а LEAVE довольно удобен. Это было измерено на ядре 2.6 серии.
Те же эксперименты в ядре серии 4.4 и ряда 3.14 показали нулевое использование либо LEAVE, либо ENTER. Предположительно, генерация кода gcc для новых gcc, используемых для компиляции этих ядер, прекратила выдавать LEAVE (или параметры машины заданы по-разному).