Прошлое
Повествование можно начать с 1962 г., когда в Кембриджском университете началась работа над CPL («Cambridge Programming Language») — «усовершенствованным вариантом» ALGOL-60. К работе над языком подключился аспирант Мартин Ричардс; главной сложностью в реализации нового ЯП ему показалась необходимость ручного портирования компилятора для разных компьютерных платформ. В частности, когда кембриджский EDSAC-2 заменили на Atlas-2, разработчики CPL потратили много времени на портирование своего компилятора для новой платформы.
Диссертация Мартина была посвящена «само-компилирующемуся» CPL: разработанный Мартином компилятор был написан на сильно упрощённом варианте CPL, компилятор которого несложно было написать на тогдашнем макроассемблере. Перенос CPL на новую платформу теперь можно было выполнить в два шага:
- Вручную пишем компилятор «упрощённого CPL»;
- Компилируем им компилятор «полного CPL».
На этом Мартин не остановился, и разработал BCPL — систему для разработки переносимых компиляторов. Компилятор BCPL генерировал псевдокод, названный Мартином «OCODE».
OCODE выглядел примерно так:
(Для экономии места, последовательности команд записаны в одну строчку. Мартин в своём руководстве по BCPL поступает точно так же.)
Исходный код на BCPL:
Среди «универсальных машинных языков» OCODE уникален тем, что метки в нём определяются специальными инструкциями — т.е. для интерпретации программы её нужно сначала всю загрузить в память, и найти в ней метки.
— а отдельная программа, кодогенератор, превращала файл с таким псевдокодом в исполнимую программу для конечного процессора. OCODE сохранялся в виде текстового файла из десятичных чисел, разделённых пробелами и переводами строк: в то время, когда OCODE разрабатывался, привязка формата файла к конкретному размеру байта ограничивала бы переносимость такого файла.OCODE | «расшифровка» («procode») | |
---|---|---|
94 5 L1 83 73 69 86 69 95 4 42 0 42 0 40 2 14 83 42 0 42 1 40 2 14 83 42 2 40 3 42 1 15 92 85 L5 90 L6 42 1 40 4 40 2 14 83 40 4 42 1 14 80 4 90 5 40 4 40 5 88 L6 91 4 42 2 40 3 42 1 15 92 85 L7 90 L8 40 4 40 2 14 8 87 L9 40 4 42 2 11 92 85 L11 90 L10 42 0 40 6 40 2 14 83 40 4 40 6 14 80 6 90 L11 40 6 40 3 22 86 L10 91 6 90 L9 40 4 42 1 14 80 4 90 L7 40 4 40 5 88 L8 91 4 97 103 0 |
ENTRY 5 L1 'S' 'I' 'E' 'V' 'E' SAVE 4 LN 0 LN 0 LP 2 PLUS STIND LN 0 LN 1 LP 2 PLUS STIND LN 2 LP 3 LN 1 MINUS STORE JUMP L5 LAB L6 LN 1 LP 4 LP 2 PLUS STIND LP 4 LN 1 PLUS SP 4 LAB L5 LP 4 LP 5 ENDFOR L6 STACK 4 LN 2 LP 3 LN 1 MINUS STORE JUMP L7 LAB L8 LP 4 LP 2 PLUS RV JF L9 LP 4 LN 2 MULT STORE JUMP L11 LAB L10 LN 0 LP 6 LP 2 PLUS STIND LP 4 LP 6 PLUS SP 6 LAB L11 LP 6 LP 3 LS JT L10 STACK 6 LAB L9 LP 4 LN 1 PLUS SP 4 LAB L7 LP 4 LP 5 ENDFOR L8 STACK 4 RTRN ENDPROC 0 |
; стековый кадр (два параметра и две локальные переменные) ; поместить на стек число 0 ; поместить ещё один 0, прибавить к нему 2-ой элемент стека ; записать в массив на вершине стека значение под ним ; всё то же самое для 1-ого элемента массива ; поместить на стек число 2 ; вычесть единицу из значения 3-его элемента стека ; записать результат в локальную переменную ; перейти к метке L5 ; объявление метки L6 ; взять 4-ый элемент стека, записать в массив по этому индексу 1 ; прибавить к 4-ому элементу стека 1, записать результат обратно ; L5: перейти к метке L6, если 4-ый элемент стека <= 5-ому ; объявление, что на стеке сейчас четыре элемента ; вычесть единицу из значения 3-его элемента стека ; перейти к метке L7 ; L8: сложить 4-ый и 2-ой элементы стека ; прочитать значение по этому адресу; если это 0, перейти к L9 ; умножить 4-ый элемент на два ; перейти к метке L11 ; объявление метки L10 ; взять 6-ой элемент стека, записать в массив по этому индексу 0 ; прибавить к 6-ому элементу стека 4-ый, записать рез-т обратно ; объявление метки L11 ; перейти к метке L10, если 7-ой элемент стека меньше 4-ого ; на стеке сейчас шесть элементов; объявление метки L9 ; прибавить к 4-ому элементу стека 1, записать результат обратно ; L10: перейти к L8, если 4-ый элемент стека <= 5-ому ; на стеке четыре элемента; окончание процедуры |
Исходный код на BCPL:
В более новых версиях OCODE добавилась поддержка чисел с плавающей точкой (соответственно, набор поддерживаемых опкодов почти удвоился), а также удалили опкодLET sieve(workvec, vecsize) BE { workvec!0 := 0 workvec!1 := 0 FOR i = 2 TO vecsize-1 DO workvec!i := 1 FOR i = 2 TO vecsize-1 DO IF workvec!i DO { LET j = 2 * i WHILE j < vecsize DO { workvec!j := 0 j := j + i } } }
ENDFOR
— вместо него генерируется пара LE JT
.Среди «универсальных машинных языков» OCODE уникален тем, что метки в нём определяются специальными инструкциями — т.е. для интерпретации программы её нужно сначала всю загрузить в память, и найти в ней метки.
Компилятор BCPL(1) поставлялся в виде OCODE, и чтобы перенести его на новую платформу, нужно было:
- Вручную написать интерпретатор псевдокода(2) (на любом языке, хоть на Бейсике);
- Адаптировать кодогенератор,(3) написанный на BCPL, для своей платформы;
- Запустить под интерпретатором (2) компилятор BCPL (1), скормить ему кодогенератор (3), и получить на выходе исполнимый файл кодогенератора(4);
- Интерпретатор (2) нам с этого момента больше не нужен.
- Прогнать через кодогенератор (4) псевдокод компилятора (1), и получить на выходе исполнимый файл компилятора.
Такой подход означал, что для переноса компилятора на новую платформу требуется лишь самый минимум низкоуровневого программирования; и действительно, реализация BCPL была завершена к 1967 г. — раньше, чем была завершена реализация CPL, начатая на несколько лет раньше!
Достоинства BCPL применительно к системному программированию вдохновили Кена Томпсона на создание языка Би, а тот — коллегу Кена, Денниса Ритчи, на создание Си. Именно из BCPL пошла традиция обозначать
{
фигурными скобками}
блоки программы, и именно на BCPL была написана первая программа «Hello, World!».Более важная нам причина, по которой BCPL вошёл в историю: OCODE — первая универсальная «архитектура набора команд» (ISA), т.е. «виртуальная машина», не привязанная ни к какой конкретной аппаратной платформе с её особенностями. BCPL, таким образом — первый язык программирования, соответствующий парадигме «Write once, run anywhere» (WORA): программу на BCPL можно распространять в скомпилированном виде, и её можно будет запустить на любой платформе, для которой существует OCODE-кодогенератор.GET "libhdr" LET start() = VALOF { writef("Hello*n") RESULTIS 0 }
Сам BCPL и его OCODE так и не завоевали популярности за пределами Соединённого Королевства, но идея WORA прижилась. В 1974 г. уже на Континенте, в ETH Zürich, под руководством Никлауса Вирта был разработан «Pascal-P» — с компиляцией в промежуточный «p-код», и затем компиляцией этого «p-кода» в исполнимый код для конкретной аппаратной платформы. На основе Pascal-P уже по другую сторону океана, в UCSD, была создана ОС p-System (1978), где кодогенератора не было вообще: паскалевский p-код сам являлся целевой платформой компилятора, а ядро ОС фактически было интерпретатором p-кода, работающим «на голом железе». Все системные утилиты p-System поставлялись в виде кроссплатформенного p-кода, и для адаптации p-System к новой аппаратной платформе достаточно было написать для неё интерпретатор p-кода — перекомпилировать нечего!
Пример p-кода
Исходный код на Паскале:
Для компактности p-кода у многих инструкций есть «сокращённые опкоды» для небольших, часто используемых значений аргументов.
0000: D8 p2_0: SLDL 1 0001: 00 SLDC 0 0002: 9A STO 0003: 02 SLDC 2 0004: CC 03 STL 3 0006: DA p2_2: SLDL 3 0007: 31 SLDC 49 0008: C8 LEQI 0009: A1 12 FJP p2_5 000B: D8 SLDL 1 000C: DA SLDL 3 000D: 01 SLDC 1 000E: 32 SLDC 50 000F: 88 CHK 0010: 01 SLDC 1 0011: 95 SBI 0012: A4 01 IXA 1 0014: 01 SLDC 1 0015: 9A STO 0016: DA SLDL 3 0017: 01 SLDC 1 0018: 82 ADI 0019: CC 03 STL 3 001B: B9 F6 UJP p2_2 001D: 02 p2_5: SLDC 2 001E: CC 03 STL 3 0020: DA p2_4: SLDL 3 0021: 31 SLDC 49 0022: C8 LEQI 0023: A1 2F FJP p2_1 0025: D8 SLDL 1 0026: DA SLDL 3 0027: 01 SLDC 1 0028: 32 SLDC 50 0029: 88 CHK 002A: 01 SLDC 1 002B: 95 SBI 002C: A4 01 IXA 1 002E: F8 SIND 0 002F: A1 1C FJP p2_6 0031: 02 SLDC 2 0032: DA SLDL 3 0033: 8F MPI 0034: CC 02 STL 2 0036: D9 p2_3: SLDL 2 0037: 32 SLDC 50 0038: C9 LESI 0039: A1 12 FJP p2_6 003B: D8 SLDL 1 003C: D9 SLDL 2 003D: 01 SLDC 1 003E: 32 SLDC 50 003F: 88 CHK 0040: 01 SLDC 1 0041: 95 SBI 0042: A4 01 IXA 1 0044: 00 SLDC 0 0045: 9A STO 0046: D9 SLDL 2 0047: DA SLDL 3 0048: 82 ADI 0049: CC 02 STL 2 004B: B9 F4 UJP p2_3 004D: DA p2_6: SLDL 3 004E: 01 SLDC 1 004F: 82 ADI 0050: CC 03 STL 3 0052: B9 F2 UJP p2_4 0054: AD 00 p2_1: RNP 0 |
; поместить на стек число 0 ; записать значение на вершине стека по адресу, находящемуся под ним ; поместить на стек число 2 ; записать значение на вершине стека в стековый кадр (в локальную переменную) ; поместить на стек локальную переменную ; меньше или равна 49? ; если нет, перейти к метке p2_5 ; проверить границы массива: локальная переменная №3 должна быть между 1 и 50 ; вычесть 1 из значения на вершине стека, т.е. из локальной переменной №3 ; вычислить указатель на элемент массива: индекс — предыдущий результат вычитания, ; база — первый параметр, размер элемента — одно слово ; записать единицу по вычисленному указателю ; прибавить единицу к значению локальной переменной №3 ; записать результат обратно в стековый кадр ; безусловный переход к метке p2_2 ; записать двойку в локальную переменную №3 ; эта переменная меньше или равна 49? ; если нет, перейти к метке p2_1 ; проверить границы массива: локальная переменная №3 должна быть между 1 и 50 ; вычислить указатель на элемент массива, в точности как и выше ; прочитать слово по смещению 0 от вычисленного указателя ; если прочитано нулевое значение, перейти к метке p2_5 ; умножить значение локальной переменной №3 на два ; записать результат внутрь стекового кадра ; это значение меньше 50? ; если нет, перейти к метке p2_6 ; проверить границы массива: локальная переменная №2 должна быть между 1 и 50 ; вычислить указатель на элемент массива: индекс на единицу меньше ; значения локальной переменной №2, база и размер — как выше ; записать ноль по вычисленному указателю ; прибавить к локальной переменной №2 значение переменной №3 ; записать результат обратно в стековый кадр ; безусловный переход к метке p2_3 ; прибавить единицу к значению локальной переменной №3 ; записать результат обратно в стековый кадр ; безусловный переход к метке p2_4 ; вернуть вызывающей процедуре 0 значений со стека |
Исходный код на Паскале:
По сравнению с OCODE, который поддерживал работу только с целыми и с плавающими числами размером в машинное слово, p-код Паскаля поддерживает куда более разнообразные типы данных — строки, массивы, «упакованные массивы» (с размером элемента менее чем в одно слово), записи, множества и т.д.; а также динамическое выделение памяти. Другое существенное различие в том, что «рабочий стек» процедуры и «стековый кадр» с параметрами и локальными переменными теперь разделены.const data_size = 50; type data_array = array[1..data_size] of boolean; procedure sieve(var workvec: data_array); var i, j: integer; begin workvec[1] := false; for i := 2 to data_size-1 do workvec[i] := true; for i := 2 to data_size-1 do if workvec[i] then begin j := 2 * i; while j < data_size do begin workvec[j] := false; j := j + i; end end end;
Для компактности p-кода у многих инструкций есть «сокращённые опкоды» для небольших, часто используемых значений аргументов.
p-System оказалась достаточно популярной: несколько моделей Apple II и несколько компьютеров от IBM, в том числе оригинальный IBM PC (1981), предлагались с p-System в качестве ОС; а начиная с 1979 г., Western Digital выпускала миникомпьютеры Pascal MicroEngine, в которых выполнение p-кода было реализовано аппаратно. Таким образом p-код из абстрактного «универсального машинного языка» превратился в настоящий машинный язык настоящего компьютера.
Хотя к девяностым p-System уже исчезла с рынка, она успела вдохновить Джеймса Гослинга на создание в 1996 г. виртуальной машины Java. «Write once, run anywhere!» превратилось в раскрученный рекламный слоган, а реализации «универсального машинного языка» («байткода») развивались параллельно в двух направлениях:
- Ускорение программной реализации (JIT-компиляция в JDK 1.1, 1997 г.)
- Попытки аппаратной реализации:
- Sun microJava701 (1998)
- Sun MAJC 5200 (1999)
- aJile aJ-100 (2000)
- Imsys Cjip (2000)
- ARM Jazelle (2001)
Неудивительно, что ни один из «байткод-процессоров» — ни для p-кода, ни для Java — не стал коммерчески успешным. (Сюда же можно отнести и намного более ранний процессор Intel iAPX 432 (1981) — аппаратную реализацию байткода для Ады.) Байткод Java, как и p-код Вирта, как и OCODE — все они стек-ориентированные, потому что именно такой промежуточный код проще всего генерировать и проще всего интерпретировать. Стек «виртуальной машины» не ограничен; у «железного» процессора, напротив, фиксированное число регистров, и одна из самых важных задач компилятора — распределить данные по регистрам так, чтобы обращения к памяти выполнялись как можно реже. Для того, чтобы «в железе» отслеживать зависимости между данными, распределять их по «железным» регистрам, и переставлять обращения к ним так, чтобы уложиться в «железные» ограничения — требуются очень сложные механизмы. Получается, что при одном и том же уровне полупроводниковых технологий эффективнее создать процессор для простой ISA, и на нём реализовать трансляцию байткода, — чем выполнять этот же байткод «в железе». Вновь и вновь мечтательные айти-предприниматели убеждались, что превратить «универсальный машинный язык» в настоящий — хоть и возможно технически, но коммерчески бесперспективно.
Пример байткода Java
Исходный код:
Байткод ещё в большей степени, чем p-код Вирта, спроектирован быть компактным: например,
2a be 3c 2a 03 03 54 2a 04 03 54 05 3d 1c 1b a2 00 0d 2a 1c 04 54 84 02 01 a7 ff f4 05 3d 1c 1b a2 00 23 2a 1c 33 99 00 17 05 1c 68 3e 1d 1b a2 00 0e 2a 1d 03 54 1d 1c 60 3e a7 ff f3 84 02 01 a7 ff de b1 |
private static void sieve(boolean[]); Code: 0: aload_0 // поместить на стек ссылку, взятую из стекового кадра по индексу 0, т.е. параметр 1: arraylength // поместить на стек длину переданного массива 2: istore_1 // сохранить в стековый кадр по индексу 1 целое число со стека 3: aload_0 4: iconst_0 // поместить на стек целое число -- константу 0 5: iconst_0 6: bastore // записать в байтовый или булев массив ноль по нулевому индексу 7: aload_0 8: iconst_1 9: iconst_0 10: bastore // записать в тот же самый массив ноль по индесу 1 11: iconst_2 12: istore_2 // сохранить в стековый кадр по индексу 2 целое значение 2 13: iload_2 // поместить на стек только что сохранённое значение 14: iload_1 // поместить на стек локальную переменную №1 15: if_icmpge 28 // сравнить два целых числа; если значение >= переменной, перейти к инструкции 28 18: aload_0 19: iload_2 20: iconst_1 21: bastore // записать в байтовый или булев массив единицу по индексу из локальной переменной 22: iinc 2, 1 // увеличить локальную переменную №2 на единицу 25: goto 13 // перейти к инструкции 13 28: iconst_2 29: istore_2 // записать в локальную переменную №2 целое значение 2 30: iload_2 31: iload_1 32: if_icmpge 67 // если переменная №2 больше или равна переменной №1, перейти к инструкции 67 35: aload_0 36: iload_2 37: baload // прочитать элемент байтового или булева массива по индексу из этой переменной 38: ifeq 61 // если прочитанный элемент равен нулю, перейти к инструкции 61 41: iconst_2 42: iload_2 43: imul // умножить значение локальной переменной №2 на два 44: istore_3 // сохранить результат в стековый кадр по индексу 3 45: iload_3 46: iload_1 47: if_icmpge 61 // если локальная переменная №3 >= переменной №1, перейти к инструкции 61 50: aload_0 51: iload_3 52: iconst_0 53: bastore // записать в байтовый или булев массив ноль по индексу из переменной №3 54: iload_3 55: iload_2 56: iadd // прибавить к значению локальной переменной №3 переменную №2 57: istore_3 // записать результат обратно в стековый кадр 58: goto 45 // перейти к инструкции 45 61: iinc 2, 1 // увеличить локальную переменную №2 на единицу 64: goto 30 // перейти к инструкции 30 67: return // вернуться из процедуры |
Исходный код:
private static void sieve(boolean[] workvec) {
int vecsize = workvec.length;
workvec[0] = false;
workvec[1] = false;
for(int i = 2; i<vecsize; i++)
workvec[i] = true;
for(int i = 2; i<vecsize; i++)
if(workvec[i]) {
int j = 2 * i;
while(j < vecsize) {
workvec[j] = false;
j = j + i;
}
}
}
Байткод ещё в большей степени, чем p-код Вирта, спроектирован быть компактным: например,
iflt
(сравнение с нулём и условный переход) соответствует тройке инструкций SLDC 0; GEQI; FJP
, а iinc
заменяет четыре инструкции SLDL; SLDC; ADI; STL
.Есть, впрочем, и противоположный пример. Производителю процессоров гораздо выгоднее реализовать популярную ISA, чтобы на новом процессоре могли выполняться существующие программы, — чем создавать новую ISA, компиляторы для неё, и дожидаться появления необходимого минимума ПО под новую платформу. Таким «универсальным машинным языком» долгое время оставался IA-32: объём существующего ПО перевешивал возможные выгоды от перехода на новую ISA, и начиная с Pentium Pro (1995), в процессорах Intel, по сути, реализована «аппаратная эмуляция» старой ISA. Инструкции IA-32, действующие над восемью «классическими» регистрами, транслируются процессором во внутренние RISC-микрокоманды, действующие над 40 «железными» регистрами; и конвейеры внутри процессора выполняют эти RISC-микрокоманды в несколько потоков — произвольно переставляя микрокоманды, если это не нарушает зависимостей между данными. В процессоре Transmeta Crusoe (2000), в группу разработчиков которого входил Линус Торвальдс, идея аппаратной эмуляции ISA была развита ещё дальше: «из коробки» он поддерживал IA-32, но его можно было перепрограммировать для работы с любой другой ISA — для демонстрации этого у Transmeta был «рекламный образец» Crusoe с аппаратной поддержкой байткода Java.
Пример машинного кода IA-32
Обратите внимание, насколько такой код компактнее, чем в любом из предшествующих примеров.
Машинный код | Ассемблер |
---|---|
55 89 e5 56 66 c7 01 00 00 b8 02 00 00 00 be 02 00 00 00 eb 05 c6 04 31 01 46 39 d6 7c f7 eb 01 40 39 d0 7d 17 80 3c 01 00 74 f5 8d 34 00 eb 06 c6 04 31 00 01 c6 39 d6 7c f6 eb e4 5e 5d c3 |
.type _ZL5sievePbi,@function _ZL5sievePbi: # указатель на массив передан в ecx, длина массива -- в edx # %entry pushl %ebp # сохранить предыдущее значение ebp movl %esp, %ebp # теперь ebp указывает на текущий стековый кадр pushl %esi # сохранить предыдущее значение esi movw $0, (%ecx) # записать нулевое слово по адресу ecx movl $2, %eax # (т.е. два элемента массива обнулены одной инструкцией) movl $2, %esi # сохранить двойку в eax и в esi jmp .LBB1_1 # перейти к метке .LBB1_1 .LBB1_2: # %for.body movb $1, (%ecx,%esi) # записать единичный байт по адресу ecx+esi incl %esi # увеличить esi на единицу .LBB1_1: # %for.cond cmpl %edx, %esi # сравнить esi и edx jl .LBB1_2 # если edx < esi, перейти к метке .LBB1_2 jmp .LBB1_4 # иначе перейти к метке .LBB1_4 .LBB1_3: # %for.inc.11 incl %eax # увеличить eax на единицу .LBB1_4: # %for.cond.4 cmpl %edx, %eax # сравнить eax и edx jge .LBB1_9 # если edx >= esi, перейти к метке .LBB1_9 # %for.body.7 cmpb $0, (%ecx,%eax) # сравнить с нулём байт по адресу ecx+esi je .LBB1_3 # если равен нулю, перейти к .LBB1_3 # %if.then leal (%eax,%eax), %esi # записать в esi удвоенное значение eax jmp .LBB1_7 # перейти к метке .LBB1_7 .LBB1_8: # %while.body movb $0, (%ecx,%esi) # записать нулевой байт по адресу ecx+esi addl %eax, %esi # прибавить eax к esi .LBB1_7: # %while.cond cmpl %edx, %esi # сравнить esi и edx jl .LBB1_8 # если edx < esi, перейти к метке .LBB1_8 jmp .LBB1_3 # иначе перейти к метке .LBB1_3 .LBB1_9: # %for.cond.cleanup.6 popl %esi # восстановить прежнее значение esi popl %ebp # восстановить прежнее значение ebp retl # вернуться из процедуры |
Вряд ли случайно то, что в единственных до сих пор успешных примерах аппаратной реализации «универсального машинного языка» реализованная ISA была не стековой, а регистровой. Мартин Ричардс на замену своему OCODE сам разработал новую регистровую уни-ISA под названием INTCODE (1972). INTCODE крайне прост: поддерживаются шесть регистров, восемь операций, и несколько режимов адресации; инструкции, в зависимости от режима адресации, занимают либо одно, либо два слова. В 1980 г. Мартином был разработан вариант этой уни-ISA, предназначенный для 16-битных микрокомпьютеров. Новая уни-ISA — по-прежнему с шестью регистрами, но с более сложным и менее ортогональным набором команд — получила название Cintcode, и отличалась крайней компактностью: в рекламных проспектах утверждалось, что программы на Cintcode обычно занимают втрое меньше памяти, чем будучи скомпилированными в машинные коды 6502. Для компьютеров BBC Micro и Amiga пользовались популярностью ОС, поддерживающие выполнение Cintcode наравне с машинным кодом.
Пример Cintcode
Регистр
Этот код получен при помощи свежей (2014) версии BCPL Cintcode System из приведённой выше реализации на BCPL. Компактности такого кода способствует наличие в Cintcode инструкций двойного присваивания (регистрам A и B сразу) и двойного разыменования (адрес данного получается сложением регистра A и значения в стеке).
P
— указатель стека; на входе в функцию P[3]
содержит указатель на массив, P[4]
— его длину.0: 10 L0 ; A := 0 1: DB ST0P3 ; P[3][0] := A (обнулить нулевой элемент массива) 2: DD ST1P3 ; P[3][1] := A (обнулить первый элемент массива) 3: 0F LM1 ; A := -1 4: C4 AP4 ; A := A + P[4] (на единицу меньше длины массива) 5: A6 SP6 ; P[6] := A (сохранить в локальную переменную) 6: 12 L2 ; B := A; A := 2 7: A5 SP5 ; P[5] := A 8: 5C0A JLS 10 ; IF B<A GOTO 19 10: 11 L1 ; A := 1 11: 83 LP3 ; B := A; A := P[3] 12: 9A STP5 ; P[5][A] := B (записать единицу по индексу из P[5]) 13: B5 XCH ; A := B 14: C5 AP5 ; A := A + P[5] 15: A5 SP5 ; P[5] := A (увеличить значение P[5] на единицу) 16: 86 LP6 ; B := A; A := P[6] 17: 9CF8 JLE -8 ; IF B<=A GOTO 10 19: 0F LM1 ; A := -1 20: C4 AP4 ; A := A + P[4] 21: A6 SP6 ; P[6] := A (та же самая верхняя граница цикла) 22: 12 L2 ; B := A; A := 2 23: A5 SP5 ; P[5] := A 24: 5C1B JLS 27 ; IF B<A GOTO 52 26: 83 LP3 ; A := P[3] 27: D8 RVP5 ; A := P[5][A] (прочесть элемент по индексу из P[5]) 28: 1E11 JEQ0 17 ; IF A=0 GOTO 46 30: 85 LP5 ; A := P[5] 31: 12 L2 ; B := A; A := 2 32: 34 MUL ; A := A * B (удвоенное значение P[5]) 33: A7 SP7 ; P[7] := A 34: 84 LP4 ; B := A; A := P[4] (в A длина массива) 35: BC0A JGE 10 ; IF B>=A GOTO 46 37: 10 L0 ; A := 0 38: 87 LP7 ; B := A; A := P[7] 39: 98 STP3 ; P[3][A] := B (записать нуль по индексу из P[7]) 40: 87 LP7 ; A := P[7] 41: C5 AP5 ; A := A + P[5] 42: A7 SP7 ; P[7] := A (увеличить значение P[7] на P[5]) 43: 84 LP4 ; B := A; A := P[4] (в A длина массива) 44: 5CF8 JLS -8 ; IF B<A GOTO 37 46: 11 L1 ; A := 1 47: C5 AP5 ; A := A + P[5] 48: A5 SP5 ; P[5] := A (увеличить значение P[5] на единицу) 49: 86 LP6 ; B := A; A := P[6] (A на 1 меньше длины массива) 50: 9CE7 JLE -25 ; IF B<=A GOTO 26 52: 7B RTN
Этот код получен при помощи свежей (2014) версии BCPL Cintcode System из приведённой выше реализации на BCPL. Компактности такого кода способствует наличие в Cintcode инструкций двойного присваивания (регистрам A и B сразу) и двойного разыменования (адрес данного получается сложением регистра A и значения в стеке).
Вершиной развития стековых уни-ISA — или, если посмотреть на это с другой стороны, тупиком в их развитии — стал MSIL (2001; официально называется CIL). MSIL крайне похож на байткод Java, но добавляет ряд дополнительных возможностей. Попыток реализовать MSIL аппаратно, насколько я знаю, не предпринималось; Microsoft даже не попыталась предложить альтернативу Java для создания кроссплатформенных, полнофункциональных веб-приложений. MSIL так и остался «машинным языком Microsoft-овских платформ», и никаких других.
Пример MSIL
Исходный код на C# отличается от приведённого выше примера на Java лишь заменой
С другой стороны, в MSIL нет «макроинструкций», подобных
.method private hidebysig static void sieve(bool[] workvec) cil managed { .maxstack 3 .locals init ([0] int32 vecsize, [1] int32 i, [2] int32 V_2, [3] int32 j) IL_0000: /* 02 | */ ldarg.0 // поместить на стек параметр №0 (единственный) IL_0001: /* 8E | */ ldlen // вычислить длину массива (беззнаковое целое) IL_0002: /* 69 | */ conv.i4 // преобразовать её в int32 (целое со знаком) IL_0003: /* 0A | */ stloc.0 // сохранить результат в локальную переменную №0 IL_0004: /* 02 | */ ldarg.0 // поместить на стек параметр IL_0005: /* 16 | */ ldc.i4.0 // поместить на стек константу 0 (целое со знаком) IL_0006: /* 16 | */ ldc.i4.0 IL_0007: /* 9C | */ stelem.i1 // записать в массив байтов ноль по нулевому индексу IL_0008: /* 02 | */ ldarg.0 IL_0009: /* 17 | */ ldc.i4.1 // поместить на стек константу 1 (целое со знаком) IL_000a: /* 16 | */ ldc.i4.0 IL_000b: /* 9C | */ stelem.i1 // записать в массив байтов (int8) ноль по индексу 1 IL_000c: /* 18 | */ ldc.i4.2 IL_000d: /* 0B | */ stloc.1 // записать двойку в локальную переменную №1 IL_000e: /* 2B | 08 */ br.s IL_0018 // короткий безусловный переход к инструкции IL_0019 IL_0010: /* 02 | */ ldarg.0 IL_0011: /* 07 | */ ldloc.1 // поместить на стек значение локальной переменной IL_0012: /* 17 | */ ldc.i4.1 IL_0013: /* 9C | */ stelem.i1 // записать в массив байтов единицу по этому индексу IL_0014: /* 07 | */ ldloc.1 IL_0015: /* 17 | */ ldc.i4.1 IL_0016: /* 58 | */ add // прибавить единицу к значению локальной переменной IL_0017: /* 0B | */ stloc.1 // сохранить результат обратно в переменную IL_0018: /* 07 | */ ldloc.1 IL_0019: /* 06 | */ ldloc.0 // поместить на стек значение переменной №0 IL_001a: /* 32 | F4 */ blt.s IL_0010 // если переменная №1 < переменной №0, перейти к IL_0010 IL_001c: /* 18 | */ ldc.i4.2 IL_001d: /* 0C | */ stloc.2 // записать двойку в локальную переменную №2 IL_001e: /* 2B | 1B */ br.s IL_003b // короткий безусловный переход к инструкции IL_003b IL_0020: /* 02 | */ ldarg.0 IL_0021: /* 08 | */ ldloc.2 IL_0022: /* 90 | */ ldelem.i1 // прочесть элемент массива по индексу из переменной IL_0023: /* 2C | 12 */ brfalse.s IL_0037 // если элемент равен нулю, перейти к IL_0037 IL_0025: /* 18 | */ ldc.i4.2 IL_0026: /* 08 | */ ldloc.2 IL_0027: /* 5A | */ mul // умножить значение локальной переменной №2 на два IL_0028: /* 0D | */ stloc.3 // сохранить результат сравнения в переменную №3 IL_0029: /* 2B | 08 */ br.s IL_0033 // короткий безусловный переход к инструкции IL_0033 IL_002b: /* 02 | */ ldarg.0 IL_002c: /* 09 | */ ldloc.3 IL_002d: /* 16 | */ ldc.i4.0 IL_002e: /* 9C | */ stelem.i1 // записать 0 в массив по индексу из переменной №3 IL_002f: /* 09 | */ ldloc.3 IL_0030: /* 08 | */ ldloc.2 IL_0031: /* 58 | */ add // прибавить к значению переменной №2 переменную №2 IL_0032: /* 0D | */ stloc.3 // сохранить результат в переменную №2 IL_0033: /* 09 | */ ldloc.3 IL_0034: /* 06 | */ ldloc.0 IL_0035: /* 32 | F4 */ blt.s IL_002b // если переменная №3 < переменной №0, перейти к IL_002b IL_0037: /* 08 | */ ldloc.2 IL_0038: /* 17 | */ ldc.i4.1 IL_0039: /* 58 | */ add // прибавить единицу к значению переменной №2 IL_003a: /* 0C | */ stloc.2 // сохранить результат обратно в переменную IL_003b: /* 08 | */ ldloc.2 IL_003c: /* 08 | */ ldloc.0 IL_003d: /* 32 | E1 */ blt.s IL_0020 // если переменная №2 < переменной №0, перейти к IL_0020 IL_003f: /* 2A | */ ret // вернуться из процедуры }
Исходный код на C# отличается от приведённого выше примера на Java лишь заменой
boolean
→bool
. Различия между Java-байткодом и MSIL чуть более заметны: во-первых, параметры функции и локальные переменные не соседствуют в едином стековом кадре, а разделены. Во-вторых, каждое значение на стеке сохраняется вместе с типом (знаковое/беззнаковое целое конкретного размера, ссылка, значение с плавающей точкой и т.д.), поэтому такие инструкции MSIL, как add
, «полиморфны», т.е. производят результат в зависимости от типа аргументов на стеке; а для преобразования значения из одного типа в другой (например, размера массива — в тип int32) применяются явные инструкции приведения типа. Усложнение семантики отдельных инструкций связано с тем, что MSIL с самого начала проектировался для JIT-компиляции, поэтому возможность эффективной интерпретации MSIL-кода не имела значения.С другой стороны, в MSIL нет «макроинструкций», подобных
iflt
или iinc
.Больше в этом тысячелетии стековых претендентов в «универсальные машинные языки» не было.
Настоящее
В 2000 Крис Латтнер, магистрант в UIUC, в качестве дипломного проекта начал разработку «универсального компилятора» LLVM, состоящего из полностью независимых «фронтендов», которые переводят код на ЯВУ в универсальное «промежуточное представление» (intermediate representation, IR), — и «бэкендов», которые переводят IR в исполнимый код для конкретных ISA.
В качестве промежуточного представления, LLVM-IR, была выбрана форма с однократными присваиваниями («SSA-форма»): каждой переменной значение присваивается единожды, и во время работы программы оно не меняется. Такая форма упрощает отслеживание зависимостей между данными, упрощает перестановку и замену отдельных инструкций, обнаружение и удаление повторяющихся или «мёртвых» инструкций, и т.д.
Пример LLVM-IR
Код разбит на базовые блоки (BB), каждый из которых начинается с метки и завершается инструкцией перехода (условного или безусловного). В середине BB метки и инструкции перехода запрещены. Точка — допустимый символ в названиях переменных и меток. Компилятор перечисляет в комментарии в первой строке BB всех его предшественников, т.е. все BB, которые завершаются переходом на данный.
Приведённый выше пример машинного кода IA-32 получен при помощи LLVM из этого же самого IR — поэтому, в частности, названия меток в обоих листингах совпадают, хотя порядок BB и изменён.
Стоит обратить внимание, что для каждой инструкции явным образом перечисляются типы всех её аргументов — никакого «полиморфизма» в стиле MSIL.
Исходный код на C++:
; Function Attrs: minsize noinline nounwind optsize define internal fastcc void @_ZL5sievePbi(i8* nocapture %workvec, i32 %vecsize) #0 { ; #0 -- ссылка на метаданные функции (частично расшифрованы выше) entry: store i8 0, i8* %workvec, align 1, !tbaa !1 ; записать нулевой байт по переданному указателю %arrayidx1 = getelementptr inbounds i8, i8* %workvec, i32 1 ; получить ссылку на первый элемент массива store i8 0, i8* %arrayidx1, align 1, !tbaa !1 ; записать нулевой байт по этой ссылке br label %for.cond ; безусловный переход к следующему BB for.cond: ; preds = %for.body, %entry %i.0 = phi i32 [ 2, %entry ], [ %inc, %for.body ] ; %i.0 принимает значение 2, если к этому BB пришли из %entry, либо значение %inc, если пришли из %for.body %cmp = icmp slt i32 %i.0, %vecsize ; сравнить %i.0 и %vecsize как знаковые целые, и записать в %cmp значение 1 либо 0 br i1 %cmp, label %for.body, label %for.cond.4 ; условный переход к %for.body либо %for.cond.4 for.body: ; preds = %for.cond %arrayidx2 = getelementptr inbounds i8, i8* %workvec, i32 %i.0 ; получить ссылку на %i.0-вой элемент массива store i8 1, i8* %arrayidx2, align 1, !tbaa !1 ; записать единичный байт по этой ссылке %inc = add nuw nsw i32 %i.0, 1 ; присвоить %inc сумму %i.0 и единицы ; nuw и nsw означают, что если произойдёт переполнение (знаковое или беззнаковое), то результат не определён br label %for.cond ; безусловный переход к предыдущему BB for.cond.4: ; preds = %for.cond, %for.inc.11 %i3.0 = phi i32 [ %inc12, %for.inc.11 ], [ 2, %for.cond ] ; %i3.0 принимает значение 2, если к этому BB пришли из %for.body, либо значение %inc12, если из %for.inc.11 %cmp5 = icmp slt i32 %i3.0, %vecsize ; сравнить %i3.0 и %vecsize как знаковые целые br i1 %cmp5, label %for.body.7, label %for.cond.cleanup.6 ; условный переход, если %i3.0 меньше %vecsize for.cond.cleanup.6: ; preds = %for.cond.4 ret void ; по окончанию цикла, вернуться из функции for.body.7: ; preds = %for.cond.4 %arrayidx8 = getelementptr inbounds i8, i8* %workvec, i32 %i3.0 %0 = load i8, i8* %arrayidx8, align 1, !tbaa !1, !range !5 ; прочесть байт из массива по индексу %i3.0 %tobool = icmp eq i8 %0, 0 ; проверить прочитанный байт на равенство нулю br i1 %tobool, label %for.inc.11, label %if.then if.then: ; preds = %for.body.7 %mul = shl nsw i32 %i3.0, 1 ; присвоить %mul удвоенное значение %i3.0 br label %while.cond ; безусловный переход к следующему BB while.cond: ; preds = %while.body, %if.then %j.0 = phi i32 [ %mul, %if.then ], [ %add, %while.body ] %cmp9 = icmp slt i32 %j.0, %vecsize ; сравнить %j.0 и %vecsize как знаковые целые br i1 %cmp9, label %while.body, label %for.inc.11 ; если %j.0 < %vecsize, перейти к %while.body while.body: ; preds = %while.cond %arrayidx10 = getelementptr inbounds i8, i8* %workvec, i32 %j.0 store i8 0, i8* %arrayidx10, align 1, !tbaa !1 ; записать единицу в %j.0-вой элемент массива %add = add nsw i32 %j.0, %i3.0 ; присвоить %add сумму %j.0 и %i3.0 br label %while.cond ; безусловный переход к предыдущему BB for.inc.11: ; preds = %while.cond, %for.body.7 %inc12 = add nuw nsw i32 %i3.0, 1 ; присвоить %inc12 сумму %i3.0 и единицы br label %for.cond.4 }
Приведённый выше пример машинного кода IA-32 получен при помощи LLVM из этого же самого IR — поэтому, в частности, названия меток в обоих листингах совпадают, хотя порядок BB и изменён.
!tbaa !1
— ссылка на относящиеся к массиву метаданные для alias analysis; !range !5
— ссылка на его же метаданные о возможном диапазоне значений (для bool
— 0 либо 1).Стоит обратить внимание, что для каждой инструкции явным образом перечисляются типы всех её аргументов — никакого «полиморфизма» в стиле MSIL.
Исходный код на C++:
static void sieve(bool workvec[], int vecsize) {
workvec[0] = false;
workvec[1] = false;
for(int i = 2; i<vecsize; i++)
workvec[i] = true;
for(int i = 2; i<vecsize; i++)
if(workvec[i]) {
int j = 2 * i;
while(j < vecsize) {
workvec[j] = false;
j = j + i;
}
}
}
Поскольку LLVM-IR разрабатывался в первую очередь как эффективный способ передачи кода из фронтенда в бэкенд, а не как уни-ISA, — то у него в роли уни-ISA есть ряд недостатков. Во-первых, особенности целевой платформы могут влиять на генерацию LLVM-IR: программа для 32-битной системы скомпилируется в другой LLVM-IR, нежели программа для 64-битной; а программа для Windows скомпилируется в другой LLVM-IR, нежели программа для Linux. Во-вторых, LLVM-IR нестабилен: любой разработчик LLVM может добавить новые инструкции или изменить значение существующих, если это позволит обеспечить более эффективную компиляцию важных ему ЯП на важных ему платформах. Это означает, что LLVM-IR, сгенерированный некой конкретной версией фронтенда, может использоваться только с соответствующей версией бэкенда, и ни с какими другими. В-третьих, LLVM-IR позволяет представлять код с «неопределённым поведением» — каждый бэкенд вправе транслировать такой код любым удобным образом, чтобы «выжать» из программы максимальную производительность. Код с неопределённым поведением на Си или C++ обычно компилируется в код с неопределённым поведением на LLVM-IR — т.е. фактически семантику такого кода определяет не фронтэнд, а бэкенд, оптимальным для конкретной платформы образом. Естественно, что код с неопределённым поведением противоречит парадигме «Write once, run anywhere», под которой продвигаются уни-ISA. Тем не менее, Google, являясь одним из активных разработчиков LLVM, сочла LLVM-IR адекватным форматом для распространения кроссплатформенных приложений, написанных на любом из множества ЯП, поддерживаемых в LLVM. Начиная с версии 31 (2013), Google Chrome включает поддержку PNaCl — подмножества LLVM-IR со стабильной семантикой инструкций и без платформо-зависимых конструкций и оптимизаций.
Казалось бы: чем PNaCl-приложения лучше Java-аплетов, придуманных на пятнадцать лет раньше для точно такой же цели — создания кроссплатформенных, полнофункциональных веб-приложений? (КДПВ в начале этого топика как раз в тему.) Мне известны два преимущества PNaCl: во-первых, LLVM-IR, по сравнению со стековым байткодом Java, упрощает оптимизацию кода, когда браузер транслирует PNaCl-приложение в машинный код конкретной целевой платформы. Второй и, насколько я понимаю, основной аргумент — открытость и лицензионная чистота LLVM: по поводу поддержки Java её хозяева (Sun и затем Oracle) судились было с каждым встречным, и в том числе с Google. LLVM и его IR, напротив, полностью открыты; и с одной стороны, Google бесплатно получает в компиляторах в свой PNaCl всякие плюшки и новые оптимизации, реализованные другими участниками разработки LLVM; с другой стороны, любые другие разработчики вправе добавить в свои браузеры поддержку PNaCl-приложений и не бояться судебных исков.
Пока что желающих последовать примеру Google и реализовать поддержку PNaCl не было. Напротив: Mozilla для той же самой цели — создания кроссплатформенных, полнофункциональных веб-приложений — разработала свою собственную уни-ISA под названием asm.js (2013). Генерация asm.js была реализована в виде нового бэкенда для LLVM. Работа над asm.js и PNaCl шла практически одновременно, и в Google Chrome поддержка asm.js появилась даже раньше, чем поддержка PNaCl — с версии 28 (2013).
asm.js реализует весьма элегантную идею: программа на нём представляет собой корректный код на (сильно урезанном) JavaScript, и соответственно, такая программа должна заработать в любом браузере с поддержкой JavaScript. С другой стороны, браузеры, способные распознать конструкции asm.js и «на лету» скомпилировать их в машинный код для целевой платформы, — выполнят такую программу во много раз быстрее, чем браузеры, исполняющие JavaScript «в лоб». Таким образом, и старые браузеры способны выполнять новый код на asm.js, и новые браузеры способны выполнять старый код на «классическом» JavaScript: если в скрипте нет пролога
"use asm";
, он выполняется «по-старинке».Пример asm.js
Тот же самый код на C++, что и выше, после компиляции при помощи Emscripten:
function __ZL5sievePbi($workvec,$vecsize) {
$workvec = $workvec|0; // объявление целочисленных параметров при помощи |0
$vecsize = $vecsize|0;
var $0 = 0, $1 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $2 = 0, $20 = 0, $21 = 0, $22 = 0, $23 = 0, $24 = 0, $25 = 0, $26 = 0;
var $27 = 0, $28 = 0, $29 = 0, $3 = 0, $30 = 0, $31 = 0, $32 = 0, $33 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $i = 0, $i1 = 0, $j = 0, label = 0, sp = 0;
sp = STACKTOP;
STACKTOP = STACKTOP + 32|0; if ((STACKTOP|0) >= (STACK_MAX|0)) abort();
$0 = $workvec;
$1 = $vecsize;
$2 = $0;
HEAP8[$2>>0] = 0; // доступ к булеву массиву при помощи >>0
$3 = $0;
$4 = ((($3)) + 1|0); // прибавление единицы к целочисленной переменной
HEAP8[$4>>0] = 0;
$i = 2;
while(1) {
$5 = $i;
$6 = $1;
$7 = ($5|0)<($6|0); // сравнение двух целочисленных переменных
if (!($7)) {
break;
}
$8 = $i;
$9 = $0;
$10 = (($9) + ($8)|0);
HEAP8[$10>>0] = 1;
$11 = $i;
$12 = (($11) + 1)|0;
$i = $12;
}
$i1 = 2;
while(1) {
$13 = $i1;
$14 = $1;
$15 = ($13|0)<($14|0);
if (!($15)) {
break;
}
$16 = $i1;
$17 = $0;
$18 = (($17) + ($16)|0);
$19 = HEAP8[$18>>0]|0; // чтение из булева массива
$20 = $19&1;
L8: do {
if ($20) {
$21 = $i1;
$22 = $21<<1;
$j = $22;
while(1) {
$23 = $j;
$24 = $1;
$25 = ($23|0)<($24|0);
if (!($25)) {
break L8;
}
$26 = $j;
$27 = $0;
$28 = (($27) + ($26)|0);
HEAP8[$28>>0] = 0;
$29 = $j;
$30 = $i1;
$31 = (($29) + ($30))|0;
$j = $31;
}
}
} while(0);
$32 = $i1;
$33 = (($32) + 1)|0;
$i1 = $33;
}
STACKTOP = sp;return;
}
Интересно, что когда asm.js в своём развитии «утыкался» в ограничения JavaScript (отсутствие поддержки 64-битных чисел, многопоточности, SIMD-инструкций и т.п.), — то недостающие конструкции добавляли в стандарт JavaScript. Таким образом, совместимость asm.js со стандартным JavaScript — результат усилий не только разработчиков asm.js, но и разработчиков стандарта языка.
Кроме уже упомянутых причин использовать вместо Java-аплетов новую технологию на основе LLVM, сторонники asm.js приводят такой аргумент: «В JavaScript есть встроенная виртуальная машина, поэтому добавление еще одной приводит к появлению второго набора API подключений, чтобы дать виртуальной машине доступ к DOM, сетям, сенсорам, устройствам ввода и т.п. За это придется кое чем пожертвовать. Например, как будут процессы в виртуальной машине распределять между собой имеющиеся ресурсы? Ответить на этот вопрос сложнее, чем кажется.» (орфография оригинала) В частности, это означает, что новые фичи движка одновременно становятся доступными из asm.js и из «классического» JavaScript; их не потребуется реализовывать дважды.
Будущее
По сравнению с «сырым» LLVM-IR, на который сделали было ставку Google и Apple, у asm.js немало преимуществ; но есть и недостатки. Во-первых, код на asm.js неимоверно увеличивается в размере — сложение двух целых чисел занимает больше двадцати байт! Во-вторых, для выполнения кода на asm.js требуется повторно выполнять его синтаксический разбор. Куда эффективнее было бы сохранять результат компиляции в виде AST, а не генерировать из него код с синтаксисом JavaScript, и потом по этому коду восстанавливать AST. Так летом 2015 появился новый «универсальный машинный язык» под названием WebAssembly, сохранивший некоторые сильные стороны asm.js (например, интеграцию с JavaScript-движком), устранивший два его названных недостатка, и отказавшийся от непосредственной совместимости со старыми JavaScript-движками (как прокомментировал vsb, «Или игра в браузере у человека не запустилась вообще, или запустилась с 1 fps — разницы нет, в любом случае он в неё играть не сможет.») Вместо этого разработчики WebAssembly реализовали «замазку» — JavaScript-библиотеку, которая читает код на WebAssembly и «на лету» генерирует из него код на «классическом» asm.js; а его старые версии браузеров уже умеют выполнять.
Пример WebAssembly
Тот же самый код на C++, что и выше, скомпилированный в WebAssembly при помощи последней версии LLVM:
Названия меток совпадают с листингом LLVM-IR и машинного кода IA-32, потому что компилятор тот же самый; генерировать байткод на WebAssembly он пока не умеет, только ассемблерный листинг.
Особо стоит отметить, что ожидания, выраженные VEG после летнего пресс-релиза — «Формат у WebAssembly будет не просто линейным набором команд.» — не оправдались.
_Z5sievePbi: .param i32 # параметры обозначаются как $0 и $1 .param i32 .local i32, i32, i32, i32 # локальные переменные обозначаются $2, $3, $4, $5 # %entry block BB0_9 # в начале каждого блока или цикла -- ссылка на конец i32.const $2, 0 # записать ноль в локальную переменную i32.store8 $0, $2 # записать значение переменной в массив i32.const $3, 1 i32.add $push0, $0, $3 # прибавить единицу к параметру--указателю на массив i32.store8 $pop0, $2 # записать ноль по полученному указателю i32.const $4, 2 set_local $5, $4 BB0_1: # %for.cond loop BB0_3 i32.ge_s $push1, $5, $1 # сравнить переменную с параметром--длиной массива br_if $pop1, BB0_3 # %for.body i32.add $push7, $0, $5 # $pushN и $popN -- "безымянные" локальные переменные i32.store8 $pop7, $3 # для немедленного использования (следующей командой) i32.add $5, $5, $3 # прибавить единицу к локальной переменной br BB0_1 BB0_3: # %for.cond.4 loop BB0_9 block BB0_8 block BB0_4 block BB0_3 # конец блока там же, где начало (?заголовок цикла?) i32.lt_s $push2, $4, $1 br_if $pop2, BB0_4 br BB0_9 BB0_4: # %for.body.7 i32.add $push3, $0, $4 i32.load8_u $5, $pop3 # прочесть элемент массива по индексу из $4 i32.eq $push4, $5, $2 # сравнить его с нулём br_if $pop4, BB0_8 # %if.then i32.shl $5, $4, $3 # сдвинуть $4 влево на бит, т.е. удвоить BB0_6: # %while.cond loop BB0_8 i32.ge_s $push5, $5, $1 br_if $pop5, BB0_8 # %while.body i32.add $push6, $0, $5 i32.store8 $pop6, $2 i32.add $5, $5, $4 br BB0_6 BB0_8: # %for.inc.11 i32.add $4, $4, $3 br BB0_3 BB0_9: # %for.cond.cleanup.6 return
Названия меток совпадают с листингом LLVM-IR и машинного кода IA-32, потому что компилятор тот же самый; генерировать байткод на WebAssembly он пока не умеет, только ассемблерный листинг.
Особо стоит отметить, что ожидания, выраженные VEG после летнего пресс-релиза — «Формат у WebAssembly будет не просто линейным набором команд.» — не оправдались.
Одним из основных разработчиков WebAssembly стала Google — мол, в новом «универсальном машинном языке» на основе LLVM будет учтён весь опыт, полученный в ходе разработки PNaCl. Сам же PNaCl они больше не собираются развивать. Другой из основных разработчиков, Mozilla, особо отмечает, что WebAssembly, в отличие от asm.js, не привязан — ни на синтаксическом, ни на семантическом уровне — ни к какому конкретному ЯП; может статься, мол, что через десяток-другой лет JavaScript отойдёт в небытие, и браузеры больше не будут поддерживать скрипты в исходном коде — вместо этого WebAssembly станет «родным» для браузеров форматом веб-приложений. Программы для ПК совершили этот «качественный скачок» в 1980-х, когда от распространения многостраничных листингов на Бейсике и Паскале разработчики ПО перешли к распространению исполнимого кода — и, наконец, стало возможно пользоваться ПК, не владея ни одним языком программирования хотя бы на уровне «как мне всё это скомпилировать и запустить?»
Большинство пользователей веб-приложений уже сейчас не имеют ни малейшего представления о JavaScript; ну и какой смысл распространять веб-приложения в виде исходного кода?
Производители процессоров для мобильных устройств оживились. Если реализовать «в железе» выполнение кода на новом «универсальном машинном языке», то производительность веб-приложений — ради выполнения которых мобильные устройства и приобретаются — может вырасти в разы! WebAssembly хоть и высокоуровневый машинный язык, но не стековый; и поэтому есть шанс, что его аппаратная реализация могла бы стать более успешной, чем предыдущие попытки реализации стековых уни-ISA. Самое главное то, что единый машинный язык для всех мобильных приложений мог бы преобразить всю экосистему — подобно тому, как x86 в своё время преобразил мир ПК. До «гегемонии» x86, программы для ПК выходили десятком более или менее совместимых версий для различных платформ — например, у Prince of Persia были отдельные официальные версии для ПК Amiga, Amstrad, Apple II, Atari, FM Towns, IBM PC, Macintosh Quadra, NEC PC-9801, SAM Coupé, и Sharp X68000; но с какого-то момента словосочетание «программа для ПК» стало обозначать «программу для x86». Точно так же, «единая мобильная ISA» отвязала бы друг от друга разработчиков ПО, производителей устройств (OEM), и производителей процессоров: OEM-ы могли бы выбирать для своих устройств процессоры от любых производителей, или даже использовать в устройствах одной линейки процессоры разных производителей; а разработчикам больше не надо было бы тратиться на перенос приложений между разными платформами. В конечном счёте выигрывает пользователь, если любая из нужных ему программ запускается на любом из его устройств.
С другой стороны, единый машинный язык накладывает на производителей процессоров жёсткие рамки: в новый процессор нельзя добавить новую фичу, пока она не будет принята в «единую ISA» и реализована остальными производителями. Далее, по мере развития единого машинного языка неизбежны ситуации, когда один из производителей будет пытаться протолкнуть в язык такие фичи, которые конкретно ему выгодны. Например, инструкция целочисленного деления в процессорах Intel генерирует исключение при делении на ноль; в процессорах ARM — возвращает в качестве результата ноль: инженеры ARM сочли, что проверку делителя лучше переложить на компилятор, и тем самым ускорить деление в тех случаях, когда делитель заведомо ненулевой. В-третьих, как быть с поддержкой графики? WebAssembly даже не пытается предложить единую ISA для графических процессоров — там расхождения между предпочтениями разных производителей ещё сильнее, чем между системами команд Intel и ARM.
Впрочем, все эти рассуждения опираются на то, что WebAssembly приживётся, а не будет заброшен через пару лет, как PNaCl, и не обособится в отдельную нишу, как Java и MSIL.
Посмотрим.
Only registered users can participate in poll. Log in, please.
Может ли аппаратная реализация какого-либо «универсального машинного кода» стать коммерчески успешной?
68.46% да165
31.54% нет76
241 users voted. 114 users abstained.