Pull to refresh

Полвека «универсальным машинным языкам» (1966—2016): прошлое, настоящее, будущее

Reading time 27 min
Views 19K
КДПВ

Прошлое


Повествование можно начать с 1962 г., когда в Кембриджском университете началась работа над CPL («Cambridge Programming Language») — «усовершенствованным вариантом» ALGOL-60. К работе над языком подключился аспирант Мартин Ричардс; главной сложностью в реализации нового ЯП ему показалась необходимость ручного портирования компилятора для разных компьютерных платформ. В частности, когда кембриджский EDSAC-2 заменили на Atlas-2, разработчики CPL потратили много времени на портирование своего компилятора для новой платформы.

Диссертация Мартина была посвящена «само-компилирующемуся» CPL: разработанный Мартином компилятор был написан на сильно упрощённом варианте CPL, компилятор которого несложно было написать на тогдашнем макроассемблере. Перенос CPL на новую платформу теперь можно было выполнить в два шага:
  1. Вручную пишем компилятор «упрощённого CPL»;
  2. Компилируем им компилятор «полного CPL».

На этом Мартин не остановился, и разработал BCPL — систему для разработки переносимых компиляторов. Компилятор BCPL генерировал псевдокод, названный Мартином «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 поступает точно так же.)

Исходный код на 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 уникален тем, что метки в нём определяются специальными инструкциями — т.е. для интерпретации программы её нужно сначала всю загрузить в память, и найти в ней метки.
— а отдельная программа, кодогенератор, превращала файл с таким псевдокодом в исполнимую программу для конечного процессора. OCODE сохранялся в виде текстового файла из десятичных чисел, разделённых пробелами и переводами строк: в то время, когда OCODE разрабатывался, привязка формата файла к конкретному размеру байта ограничивала бы переносимость такого файла.

Компилятор BCPL(1) поставлялся в виде OCODE, и чтобы перенести его на новую платформу, нужно было:
  1. Вручную написать интерпретатор псевдокода(2) (на любом языке, хоть на Бейсике);
  2. Адаптировать кодогенератор,(3) написанный на BCPL, для своей платформы;
  3. Запустить под интерпретатором (2) компилятор BCPL (1), скормить ему кодогенератор (3), и получить на выходе исполнимый файл кодогенератора(4);
    • Интерпретатор (2) нам с этого момента больше не нужен.
  4. Прогнать через кодогенератор (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-кода
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!» превратилось в раскрученный рекламный слоган, а реализации «универсального машинного языка» («байткода») развивались параллельно в двух направлениях:

Неудивительно, что ни один из «байткод-процессоров» — ни для p-кода, ни для Java — не стал коммерчески успешным. (Сюда же можно отнести и намного более ранний процессор Intel iAPX 432 (1981) — аппаратную реализацию байткода для Ады.) Байткод Java, как и p-код Вирта, как и OCODE — все они стек-ориентированные, потому что именно такой промежуточный код проще всего генерировать и проще всего интерпретировать. Стек «виртуальной машины» не ограничен; у «железного» процессора, напротив, фиксированное число регистров, и одна из самых важных задач компилятора — распределить данные по регистрам так, чтобы обращения к памяти выполнялись как можно реже. Для того, чтобы «в железе» отслеживать зависимости между данными, распределять их по «железным» регистрам, и переставлять обращения к ним так, чтобы уложиться в «железные» ограничения — требуются очень сложные механизмы. Получается, что при одном и том же уровне полупроводниковых технологий эффективнее создать процессор для простой ISA, и на нём реализовать трансляцию байткода, — чем выполнять этот же байткод «в железе». Вновь и вновь мечтательные айти-предприниматели убеждались, что превратить «универсальный машинный язык» в настоящий — хоть и возможно технически, но коммерчески бесперспективно.

Пример байткода Java


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
Регистр 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
.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 лишь заменой booleanbool. Различия между 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, которые завершаются переходом на данный.

; 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:

_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.33% да 164
31.67% нет 76
240 users voted. 114 users abstained.
Tags:
Hubs:
+49
Comments 45
Comments Comments 45

Articles