
Прошлое
Повествование можно начать с 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:
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
}
}
}
В более новых версиях OCODE добавилась поддержка чисел с плавающей точкой (соответственно, набор поддерживаемых опкодов почти удвоился), а также удалили опкод 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!».
GET "libhdr"
LET start() = VALOF
{ writef("Hello*n")
RESULTIS 0
}
Более важная нам причина, по которой BCPL вошёл в историю: OCODE — первая универсальная «архитектура набора команд» (ISA), т.е. «виртуальная машина», не привязанная ни к какой конкретной аппаратной платформе с её особенностями. BCPL, таким образом — первый язык программирования, соответствующий парадигме «Write once, run anywhere» (WORA): программу на BCPL можно распространять в скомпилированном виде, и её можно будет запустить на любой платформе, для которой существует OCODE-кодогенератор.Сам 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 значений со стека |
Исходный код на Паскале:
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;
По сравнению с OCODE, который поддерживал работу только с целыми и с плавающими числами размером в машинное слово, p-код Паскаля поддерживает куда более разнообразные типы данных — строки, массивы, «упакованные массивы» (с размером элемента менее чем в одно слово), записи, множества и т.д.; а также динамическое выделение памяти. Другое существенное различие в том, что «рабочий стек» процедуры и «стековый кадр» с параметрами и локальными переменными теперь разделены.Для компактности 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.