Что такое компиляция программы в промежуточный код

Искал инфу по языку программирования 1С, и там такая строчка была. Гугл, кроме ещё большего смятения, ничего не дал — «предварительно компилируемый» пишут не только для 1С. Так что же это всё таки такое?

Отслеживать
51k 83 83 золотых знака 262 262 серебряных знака 500 500 бронзовых знаков
задан 8 сен 2011 в 8:42
185 1 1 золотой знак 9 9 серебряных знаков 28 28 бронзовых знаков

5 ответов 5

Сортировка: Сброс на вариант по умолчанию

Предварительно компилируемый язык (в отличие от языков динамического разбора) — это тот язык, программы на котором надо сначала компилировать, потом выполнять (например C, C++). Результатом компиляции здесь будет машинный код.

К ним же относятся и такие интерпретируемые (скриптовые) языки, как Python, Perl, Ruby, ибо компиляция (так называемая «прекомпиляция», или «компиляция на лету») у них происходит автоматически перед выполнением программы самим интерпретатором. Здесь результатом предкомпиляции будет «байт-код».

Что такое компиляция, линковка? Что такое run time?

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

Источник: ru.stackoverflow.com

Промежуточный код

Байт-код (байтко́д; англ. bytecode, также иногда p-код, p-code от portable code) — стандартное промежуточное представление, в которое может быть переведена компьютерная программа автоматическими средствами. По сравнению с исходным кодом, удобным для создания и чтения человеком, байт-код — это компактное представление программы, уже прошедшей синтаксический и семантический анализ. В нём в явном виде закодированы типы, области видимости и другие конструкции. С технической точки зрения байт-код представляет собой машинно-независимый код низкого уровня, генерируемый транслятором из исходного кода.
Многие современные языки программирования, особенно интерпретируемые, используют байт-код для облегчения и ускорения работы интерпретатора. Трансляция в байт-код является методом, промежуточным по эффективности между прямой интерпретацией и компиляцией в машинный код.
По форме байт-код похож на машинный код, но предназначен для исполнения не реальным процессором, а виртуальной машиной. В качестве виртуальной машины обычно выступает интерпретатор соответствующего языка программирования (иногда дополненный JIT- или AOT-компилятором). Спецификации байт-кода и исполняющих его виртуальных машин могут сильно различаться для разных языков: часто байт-код состоит из инструкций для стековой виртуальной машины, однако могут использоваться и регистровые машины. Тем не менее, большинство инструкций байт-кода обычно эквивалентны одной или нескольким командам ассемблера.

Компиляция. Как работает компилятор


Байт-код называется так, потому что длина каждого кода операции традиционно составляет один байт. Каждая инструкция обычно представляет собой однобайтовый код операции (от 0 до 255), за которым могут следовать различные параметры, например, номер регистра или адрес в памяти.

Байт-код во многом похож на машинный код, только он использует набор инструкций не реального процессора, а виртуального. При этом он может включать в себя участки, ориентированные на использование JIT-компилятора, оптимизирующего выполнение команд под реальный процессор, на котором запущена программа.
JIT-компиляция (англ. Just-in-time compilation, компиляция «на лету») или динамическая компиляция (англ. dynamic translation) — это технология увеличения производительности программных систем, использующих байт-код, путём компиляции байт-кода в машинный код или в другой формат непосредственно во время работы программы. «Официально» в Java до 9-й версии был только JIT-компилятор. В Java 9 появился ещё один компилятор, причём компилирует он с опережением (AoT). Эта возможность позволяет компилировать классы Java в нативный код перед запуском на виртуальной машине. Данная функция предназначена для улучшения времени запуска и малых, и больших приложений, с ограниченным влиянием на максимальную производительность.
Для CISC процессоров некоторые инструкции могут объединяться в более сложные конструкции, поддерживаемые процессором, а для RISC – наоборот разбиваться на более простые последовательности команд.

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

Однако, поскольку байт-код обычно менее абстрактный, более компактный и более «компьютерный» чем исходный код, эффективность байт-кода обычно выше чем чистая интерпретация исходного кода, предназначенного для правки человеком. По этой причине, многие современные интерпретируемые языки на самом деле компилируют в байт-код и запускают интерпретатор байт-кода. К таким языкам относятся Perl, PHP и Python. Программы на Java обычно передаются на целевую машину в виде байт-кода, который перед исполнением транслируется в машинный код «на лету» — с помощью JIT-компиляции. В стандарте открытых загрузчиков Open Firmware фирмы Sun Microsystems байт код представляет операторы языка Forth.

Читайте также:
Программа сделать снимок экрана на ноутбуке

Следует заметить, что, несмотря на то, что промежуточный код любого интерпретируемого языка можно назвать байт-кодом, данный термин применяется в основном для языка Java. В Perl, Python, Ruby и других подобных языках, результат компиляции чаще непосредственно называют промежуточным кодом. В C# и других языках платформы .NET промежуточный код называется IL-кодом.

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

Источник: swinopes.livejournal.com

Компиляция. 6: промежуточный код

Первый этап — разбор синтаксиса нашего джей-скрипа — пройден; подбираемся к генерации кода.

  • его было легко генерировать;
  • его было легко обрабатывать.

Далее в посте:

  1. Выбор кода
  2. Компиляция
  3. Выполнение
  4. Backpatching

Выбор кода

Часто п-код делают стековым (например, MSIL): вообразим себе процессор, внутри которого нет пронумерованных регистров, а есть один большой стек, и все действия выполняются с верхними значениями на стеке. (Те, кому довелось программировать для х87, с такой моделью знакомы.) Стековый код действительно удобно генерировать и удобно выполнять, но не слишком удобно обрабатывать — например, в нём тяжело отслеживать зависимости между командами.

О выборе между стековым и регистровым промежуточным кодом выразительно отзывается создатель языка Beep:

Регистровый без вариантов:

Упрощение рантайма. Меньше манипуляций с указателями. Отсутствует понятие переполнения стека. Меньше кода, меньше работы с памятью — меньше места для критических ошибок.
Увеличивается сложность компиляции: появляется фаза выделения регистров. В случе исполнения на виртуальной машине нам не важно количество регистров, можем сделать их достаточное количество для того, что бы вообще не делать аллокацию, а просто маппить все параметры и переменные функции на регистры (см. Lua). Если количество параметров будет превышать количество регистров, то можно выделять часть activation record в хипе, но проще сделать так, что бы компилятор предлагал автору такого кода лечить голову.
В любом случае, если стоит вопрос упрощения рантайма ценой усложнения компилятора, так и следует поступать.
Возможность оптимизации: маппинг N регистров виртуальной машины на регистры процессора. На стековой машине это сделать значительно сложнее.

  • Все команды равной длины (у нас — по 4 байта)
  • Первый байт — номер команды (по отдельному опкоду под каждую возможную операцию)
  • Второй байт — номер регистра для результата (256 регистров достаточно любому!)
  • Остаток — либо два номера регистров-операндов, либо непосредственное значение—операнд.
  • загрузка непосредственного значения в регистр;
  • чтение из памяти в регистр;
  • запись из регистра в память;
  • условный переход;
  • вывод строки или числа;
  • ввод числа;
  • остановка.

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

Упорядочим опкоды так, чтобы команды похожей структуры (в плане используемых регистров) шли подряд, и вынесем определение структуры команды в отдельный файл jsk.h : она потребуется и компилятору, и интерпретатору.
typedef unsigned char regnum;

struct command enum opcodes hlt,
store, // dst>
jz, // dst>
echo, // dst>
mov, // >dst
load, // >dst
input, // >dst
add, // src>dst
sub, // src>dst
mul, // src>dst
div, // src>dst
eq, // src>dst
ne, // src>dst
ge, // src>dst
le, // src>dst
gt, // src>dst
lt // src>dst
>;

opcodes opcode;
regnum dest;
union struct regnum src1, src2;
>;
short imm;
>;
command(opcodes opcode, regnum dest, regnum src1, regnum src2) :
opcode(opcode), dest(dest), src1(src1), src2(src2) <>
command(opcodes opcode, regnum dest, short imm) :
opcode(opcode), dest(dest), imm(imm) <>
>;

Чтобы под опкод действительно выделялся один байт, при компиляции придётся указывать ключ

Компиляция

Строки для echo будем хранить вместе с кодом программы, в самом конце; одинаковые строки объединим, чтобы хранилась только одна копия. Для этого будем хранить map всех строк, где значением будет «идентификатор» строки (её порядковый номер в программе).

Все переменные в джей-скрипе — глобальные. В отдельном map будем хранить по имени переменной номер выделенного ей регистра.

Читайте также:
Большинство программ используют инструкции которые последовательно хранятся в памяти

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

Берём парсер, и поехали: заменяем метод print() на аналогичный по сути emit() , склеивающий списки команд дочерних узлов в один большой список.

(340 строк кода вынесены на http://tyomitch.net.ru/jsk.y.html, чтобы сохранить размер поста в пределах допустимого.)

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

Выполнение

Отобразим файл с п-кодом в память, и будем работать с ним, как с массивом структур command . Собственно выполнение — это цикл из 4 строк, и реализация функций-команд; большая же часть кода — вспомогательная шелуха.

int pc = 0 ; // индекс команды (не смещение)
bool halted = false ;
int mem[ 1000 ]; // размер не важен: всё равно пока не пользуемся

typedef int (*op)( int src1, int src2, int dest, int imm); // все возможные входные значения

const char * fdata = NULL ; // весь прочитанный п-код

extern op ops[]; // объявлен ниже

int main( int argc, char ** argv)

if (argc!= 2 ) printf( «Missing pcode file name. n » );
exit( 1 );
>

int fd = open(argv[ 1 ], O_RDONLY);
if (fd < 0 ) printf( «Cannot open pcode file. n » );
exit( 1 );
>
struct stat finfo;
fstat(fd,
fdata = ( const char *)mmap( 0 , finfo.st_size, PROT_READ, MAP_PRIVATE, fd, 0 );
if (!fdata) printf( «Cannot read pcode file. n » );
exit( 1 );
>

const command* pcode = ( const command*) fdata;

int r[ 256 ] = < 0 >; // registers

while (!halted) command next = pcode[pc++];
r[next.dest] = ops[next.opcode](r[next.src1], r[next.src2], r[next.dest], next.imm);
>

munmap(( void *)fdata, finfo.st_size);
close(fd);
return 0 ;
>

int hlt( int src1, int src2, int dest, int imm) < halted = true ; return dest; >
int store( int src1, int src2, int dest, int imm) < mem[imm] = dest; return dest; >
int jz( int src1, int src2, int dest, int imm) < if (!dest) pc+=imm; return dest; >
int echo( int src1, int src2, int dest, int imm) < if (imm) printf( » %s » , fdata+imm); else printf( » %d » , dest); return dest; >
int mov( int src1, int src2, int dest, int imm) < return imm; >
int load( int src1, int src2, int dest, int imm) < return mem[imm]; >
int input( int src1, int src2, int dest, int imm) < int d; scanf( » %d » , return d; >
int add( int src1, int src2, int dest, int imm) < return src1+src2; >
int sub( int src1, int src2, int dest, int imm) < return src1-src2; >
int mul( int src1, int src2, int dest, int imm) < return src1*src2; >
int div( int src1, int src2, int dest, int imm) < return src1/src2; >
int eq( int src1, int src2, int dest, int imm) < return src1==src2; >
int ne( int src1, int src2, int dest, int imm) < return src1!=src2; >
int ge( int src1, int src2, int dest, int imm) < return src1>=src2; >
int le( int src1, int src2, int dest, int imm) < return src1
int gt( int src1, int src2, int dest, int imm) < return src1>src2; >
int lt( int src1, int src2, int dest, int imm)

Фанфары! Запускаем первую в мире программу на джей-скрипе:

Backpatching

Сейчас в каждом узле дерева хранится список всех соответствующих ему команд, и каждая реализация emit включает в себя объединение команд из дочерних узлов — в том самом порядке (слева направо), в котором эти узлы создавались во время парсинга. Можно сэкономить и память на хранение команд, и код на их объединение, если все генерируемые команды сразу же сваливать в результат, а в символах хранить только «метаинформацию» типа номеров регистров.

Самая резкая разница — что теперь нам вообще не потребуется дерево: для каждого символа оказывается достаточно хранить один скаляр. Более того: у символов операторов теперь вовсе нет значения — весь результат их разбора немедленно сваливается в вектор готового п-кода; поэтому свёртках, где не генерируется код, даже наследовать ничего не нужно.

Небольшая проблема возникает в конструкциях типа if и while , где после проверки условия нужно вставить прыжок вперёд, если условие не выполняется; и до конца разбора конструкции неизвестно, на сколько нужно прыгать. Придётся оставить на месте прыжка команду-пустышку, и заполнять её в конце разбора. Такая система однопроходной генерации кода с пустышками, и их последующего «залатывания», называется backpatching. Она весьма универсальна, и не только позволяет компилировать все привычные управляющие конструкции, но и упрощает реализацию операторов типа break , прыгающих вперёд на неизвестное расстояние.

Читайте также:
Программа лучший год вашей жизни

Чтобы вставить прыжок-пустышку после проверки условия в if и while , добавим в грамматику маркер — «пустой символ»:

OP: ‘if’ ‘(‘ EXPR ‘)’ M OP ‘else’ M OP ; | ‘while’ ‘(‘ EXPR ‘)’ M OP ; M : ;

Трюк в том, что свёртка M выполнится после свёртки EXPR (которая сгенерирует код проверки условия) и перед свёрткой OP , так что в код свёртки M как раз и сможем добавить генерацию пустышки. Когда будем сворачивать конструкцию целиком, заполним пустышку прыжком до следующей пустышки (после if ) либо до конца всего сгенерированного кода (после else и while ).

Расстояние для прыжка вперёд теперь знаем; а как для while узнать расстояние для прыжка назад в конце цикла? Ведь раз нет списков команд, значит в коде свёртки не сможем узнать, сколько команд заняла вся конструкция. Придётся завести ещё один маркер N , который не генерирует код, а просто запоминает адрес нужного места:

OP: ‘while’ N ‘(‘ EXPR ‘)’ M OP ; M : ; N : ;

Дополнительное преимущество новой системы — что для каждой генерируемой команды сразу известен её адрес, так что для строк сможем хранить не временные «идентификаторы», а списки всех ссылок. Это упростит подстановку адресов строк на заключительном этапе генерации п-кода.

% <
#include
#include
#include
#include
extern int yylineno;
extern int yylex();
void yyerror( char *s) <
std::cerr exit( 1 );
>
#include «jsk.h»
struct arg_t <
regnum dest; // if numeric
std::string str; // if literal
> ;
typedef struct <
std::string str; // tokens
regnum dest; // expr
arg_t arg;
std::list < arg_t >args;
int addr; // markers
char null; // op (no data)
> YYSTYPE;
#define YYSTYPE YYSTYPE
#define TOKENPASTE(x, y) x ## y
#define TOKENPASTE2(x, y) TOKENPASTE(x, y)
#define foreach(i, list) typedef typeof(list) TOKENPASTE2(T, __LINE__ );
for (TOKENPASTE2(T, __LINE__ )::iterator i = list.begin(); i != list.end(); i++)
template
inline void append(L list2) <
list 1. splice(list1.end(), list2, list2.begin(), list2.end());
>

std::vector < command >pcode;
std::map vars;
std::map > strings;
regnum lastreg = 0 ;
inline regnum newreg() <
if (!++lastreg)
yyerror( «Too many registers used.» );
return lastreg;
>

inline int emit( const command
pcode.push_back(cmd);
return pcode.size()- 1 ;
>
inline int emit(command::opcodes opcode, regnum dest, regnum src1, regnum src2) <
return emit(command(opcode, dest, src1, src2));
>
inline int emit(command::opcodes opcode, regnum dest, short imm) <
return emit(command(opcode, dest, imm));
>
inline void fix( int index, command::opcodes opcode, regnum dest, short imm) <
pcode[index] = command(opcode, dest, imm);
>
%>

%token IF ELSE WHILE EXIT
%token EQ LE GE NE
%token STRING NUM ID

%type < str >ID NUM STRING
%type < dest >EXPR EXPR1 EXPR2 TERM VAL
%type < arg >ARG
%type < args >ARGS
%type < null >OPS OP1 OP2 OP
%type < addr >M N

OPS: OP // no action
| OPS OP // no action
;

OP: OP1 | OP2 ; // no action

VAL: NUM < $$ = newreg(); emit(command::mov, $$ , atoi( $1 .c_str())); >
| ‘-‘ VAL < $$ = newreg(); emit(command::sub, $$ , 0 , $2 ); >
| ‘!’ VAL < $$ = newreg(); emit(command::eq, $$ , 0 , $2 ); >
| ‘(‘ EXPR ‘)’ < $$ = $2 ; >
| ID < if (vars[ $1 ])
$$ = vars[ $1 ];
else
yyerror( «Undefined variable» );
>
| ID ‘(‘ ARGS ‘)’ < if (! $1 .compare( «input» )) <
if ( $3 .size())
yyerror( «Input: too many arguments» );
$$ = newreg();
emit(command::input, $$ , 0 );
> else if (! $1 .compare( «echo» )) <
if (! $3 .size())
yyerror( «Input: too many arguments» );
$$ = 0 ;
foreach(i, $3 )
if (!i- > dest) // string
strings[i- > str].push_back(emit(command::echo, 0 , 0 ));
else
emit(command::echo, i- > dest, 0 );
> else
yyerror( «Undefined function» );
>
;

int main() <
yyparse();
int offset = pcode.size()* sizeof (command);
foreach(i, strings) <
foreach(j, i- > second) // all refs
pcode[*j].imm = offset;
offset += i- > first.length();
offset++;
>
foreach(i, pcode) // dump code
write( 1 ,
foreach(i, strings) // dump strings
write( 1 , i- > first.c_str(), i- > first.length()+ 1 );

>

Код компилятора сократился почти вдвое, а компилирует он точно так же, как и прежде.
Если нет разницы, зачем писать больше?

  • компиляция
  • промежуточный код
  • генерация кода

Источник: habr.com

Рейтинг
( Пока оценок нет )
Загрузка ...
EFT-Soft.ru