КДПВ

Прошлое


Повествование можно начать с 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 Zurich, под руководством Никлауса Вирта был разработан «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 лишь заменой 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, которые завершаются переходом на данный.

; 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 Coupe, и Sharp X68000; но с какого-то момента словосочетание «программа для ПК» стало обозначать «программу для x86». Точно так же, «единая мобильная ISA» отвязала бы друг от друга разработчиков ПО, производителей устройств (OEM), и производителей процессоров: OEM-ы могли бы выбирать для своих устройств процессоры от любых производителей, или даже использовать в устройствах одной линейки процессоры разных производителей; а разработчикам больше не надо было бы тратиться на перенос приложений между разными платформами. В конечном счёте выигрывает пользователь, если любая из нужных ему программ запускается на любом из его устройств.

С другой стороны, единый машинный язык накладывает на производителей процессоров жёсткие рамки: в новый процессор нельзя добавить новую фичу, пока она не будет принята в «единую ISA» и реализована остальными производителями. Далее, по мере развития единого машинного языка неизбежны ситуации, когда один из производителей будет пытаться протолкнуть в язык такие фичи, которые конкретно ему выгодны. Например, инструкция целочисленного деления в процессорах Intel генерирует исключение при делении на ноль; в процессорах ARM — возвращает в качестве результата ноль: инженеры ARM сочли, что проверку делителя лучше переложить на компилятор, и тем самым ускорить деление в тех случаях, когда делитель заведомо ненулевой. В-третьих, как быть с поддержкой графики? WebAssembly даже не пытается предложить единую ISA для графических процессоров — там расхождения между предпочтениями разных производителей ещё сильнее, чем между системами команд Intel и ARM.

Впрочем, все эти рассуждения опираются на то, что WebAssembly приживётся, а не будет заброшен через пару лет, как PNaCl, и не обособится в отдельную нишу, как Java и MSIL.

Посмотрим.
Может ли аппаратная реализация какого-либо «универсального машинного кода» стать коммерчески успешной?

Проголосовало 217 человек. Воздержалось 106 человек.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Комментарии (45)


  1. StrangerInRed
    20.11.2015 09:35

    Спасибо, очень полезная статья для людей связанных с интерпретаторами и ВМ.


  1. chaetal
    20.11.2015 10:08
    +3

    Интересно, почему в подобных обзорах часто старательно обходят стороной Smalltalk-овскую ветку развития (Smalltalk-80, Squeak, Self, Strongtalk), которая привела к JIT-у, HotSpot-у и той Java, которую все и юзают? (А за статью "+" само собой)


    1. VasakaInc
      20.11.2015 13:26

      Вот тоже интересно.
      Там ещё, если мне не изменяет память, и система типов интересная.


      1. tyomitch
        20.11.2015 13:29
        +3

        Мне и самому интересно!
        Если chaetal напишет топик о той ветке, с удовольствием почитаю :)


    1. zahardzhan
      21.11.2015 16:34
      +1

      И Lisp-овскую ветку развития, начатую за 5 лет до начала повествования этой истории.


      1. chaetal
        24.11.2015 10:59

        Я вот всегда думал, что LISP здесь играл не малую роль, а после статьи полез смотреть — и навскидку ничего не нашел… Удивился, но решил, что где-то ошибался… В общем, можно ссылочки по этому поводу?


        1. zahardzhan
          24.11.2015 11:53

          Пожалуй воздержусь.


          1. chaetal
            24.11.2015 12:07

            Жаль. Я вот за 5 минут обеда :) накопал только The Evolution of Lisp, где говорится про разработку Vlisp в середине 70-х. Наверное, можно ориентироваться на это как на отправную точку?


  1. VasakaInc
    20.11.2015 13:24
    +3

    но с какого-то момента словосочетание «программа для ПК» стало обозначать «программу для x86».

    А с играми ещё смешнее. С какого-то времени словосочетание «игра для ПК» стало обозначать «игру для Windows». Просто повсеместная эпидемия. Единственное место, из виденных мной, в которых «игра под Windows» обозначает «игра под Windows», это Стим. В остальных местах просто невозможно определить существует ли версия для Линукс.
    Казалось бы, игровые интернет издания заинтересованы в правильной классификации, но на практике всё по другому.


    1. rshadow
      20.11.2015 15:57

      Издания делаются журналистами, у которых из знаний в предметной области — только собственный опыт. Соответственно они одни из основных разносчиков идей: «игра для ПК» стало обозначать «игру для Windows». Так как другого опыта нет, и образования в данной сфере тоже нет.


      1. VasakaInc
        20.11.2015 16:28

        Да я бы не сказал, что они не понимают этого. Видел много раз как на разных игровых сайтах поднимался этот вопрос. Руководство прекрасно понимает разницу, но под тем или иным предлогом оставляет всё как было.
        Возможно сайт потребует большой переделки, естественно игровая база должна быть вся переработана из за появления новой платформы, может что-то ещё. В любом случае реальная причина это не непонимание разницы между Линукс и Windows.


        1. zahardzhan
          21.11.2015 16:46
          -1

          Я уверен, что с точки зрения массового потребителя «игра для ПК» и при этом не для Windows — это блажь, потому что, насколько я могу судить из личного опыта, качество PC-игр на не-Windows платформах объективно уступает качеству тех же самых PC-игр на Windows-платформах.


          1. VasakaInc
            21.11.2015 17:21
            +5

            Если игра, например, под XBOX ONE уступает по качеству той же игре на PS4 это основание исключить платформу XBOX ONE на игровом сайте? Думаю, владельцы XBOX ONE с вами не согласятся. Как не согласны и пользователи Линукс с таким положением вещей.
            А вообще, по этому поводу вспомнился Карел Чапек:
            Сытый. Эти разговоры о нищете страшно преувеличены. Не так уж все плохо.


            1. zahardzhan
              21.11.2015 18:11
              -2

              Ключевое различие состоит в том, что если XBOX — это специализированная игровая система, то под Linux (например, Ubuntu) нормальное положение дел это когда новейшие AAA-игры запросто вешают всю ОС наглухо и адово тормозят. Помоему, исправить положение дел может только выпуск и популяризация «программно-аппаратных комплексов» типа стим-машин, специально приспособленных для игр, и только после этого в «версиях под линукс» действительно появится смысл — потому что производитель будет должен гарантировать, что линукс версии — это полноценные версии игр в полном смысле этого слова, по крайней мере, если они запускаются на специально предназначенных для этого системах.


              1. VasakaInc
                21.11.2015 18:25
                +1

                Давайте подведём итог? Вы хотите сказать, что игры под windows должны называться играми для PC, я правильно вас понял?
                Если нет, то о чём вообще спор? Речь шла лишь о том, что игры под Windows должны называться играми под Windows, а игры под Линукс должны называться играми под Линукс, ни больше ни меньше.


                1. zahardzhan
                  24.11.2015 11:52
                  -5

                  Давайте подведем итог. Господа минусующие по всей видимости считают, что объективно качество PC-игр на платформе Linux не уступает качеству PC-игр на платформе Windows на среднестатистическом железе. Ну ок. Дальше не имеет смысла что-либо обсуждать. Роза пахнет розой хоть розой ее назови хоть нет. Линукс игры в массы. Линуксы на десктопы. Вендекапец близок, товарищи.


                  1. VasakaInc
                    24.11.2015 13:24
                    +4

                    Не знаю кто вас минусует (я не имею возможности ни минусовать, ни плюсовать), но со своей стороны могу сказать, что я много игр попробовал на одном и том же ноутбуке и под Виндой и под Линукс. Какого-то ощутимого падения качества или скорости работы в версиях под Линукс я не заметил.
                    Некоторые игры под Линукс не поддерживают русскую локализацию, например Civilization 5, но это же всё равно не повод игнорировать игровую платформу?

                    И ещё один момент. PC это персональный компьютер. Это железо на которое может устанавливаться любая понравившаяся пользователю ОС. Это категория уровнем повыше нежели ОС. Так что даже пользователи Mac OC могут почувствовать себя несколько дискриминированными когда их, в данном случае, игровую платформу называют Mac OC (или просто Mac), а Windows — PC.
                    Ну, а про Линукс уже написали выше.


                    1. tyomitch
                      24.11.2015 14:48

                      Так что даже пользователи Mac OC могут почувствовать себя несколько дискриминированными когда их, в данном случае, игровую платформу называют Mac OC (или просто Mac), а Windows — PC.
                      Такая уж сложилась традиция наименования.

                      Вот вам внеайтишный пример: «соединённые штаты» — это вид государственного устройства, и, например, официальное название Мексики c 1824 — Estados Unidos Mexicanos. Но так уж сложилось, что название «Estados Unidos» (без уточнений) применяется к одному конкретному государству. Чувствуют ли себя мексиканцы от этого дискриминированными?


                      1. VasakaInc
                        24.11.2015 15:12
                        +3

                        Вот вам внеайтишный пример: «соединённые штаты» — это вид государственного устройства, и, например, официальное название Мексики c 1824 — Estados Unidos Mexicanos. Но так уж сложилось, что название «Estados Unidos» (без уточнений) применяется к одному конкретному государству. Чувствуют ли себя мексиканцы от этого дискриминированными?
                        Без понятия. В Мексиканцах плохо разбираюсь.
                        Что касается пользователей Mac, то для них неудобство, конечно, чисто эстетическое. Всё же они могут узнать выходила ли нужная игра под их платформу, а так же использовать фильтры для поиска игр для их платформы (естественно, на тех сайтах, где подобное предусмотрено). В случае с Линуксом всё намного печальнее. В итоге, за такой информацией я стал ходить на Стим, а страдают только сами владельцы игровых сайтов. И в условиях когда из за традиции начинает страдать бизнес, думаю, неистово держаться за традицию не самое лучшее решение.


                    1. zahardzhan
                      25.11.2015 02:43
                      -3

                      Насколько мне известно, компания Apple не поддерживает добровольный выбор пользователем железа (из всего разнообразия PC-железа) на которое будет установлена Mac OS, в отличие от Windows и Linux. Если это не так прошу поставте мне еще минусов в назидание.


                      1. grossws
                        25.11.2015 12:02
                        +1

                        Перестаньте уже ныть про свои минусы.


                        1. zahardzhan
                          25.11.2015 14:30
                          -5

                          Перестаньте говорить мне что мне перестать а что нет, ок да?


  1. beeruser
    20.11.2015 19:31
    +3

    Intel генерирует исключение при делении на ноль; в процессорах ARM — возвращает в качестве результата ноль

    Команда целочисленного деления имеется далеко не во всех ARM и появилась сравнительно недавно
    Там где она есть, поведение контроллируется битом UFSR.DIVBYZERO (UsageFault Status Register)


  1. yomayo
    22.11.2015 18:16
    +1

    Хотелось бы дополнить автора в вопросе, почему стек-ориентированные виртуальные машины проигрывают регистровым. Потому что они медленнее. Дело в том, что стек – это область памяти, которую адресуют регистры процессора. Чтение из стека или запись в него – это работа с памятью. Когда-то обращение к памяти занимало один такт процессора. В процессе развития технологий процессоры ускорялись быстрее, чем память. Для чтения/записи области памяти (и стека тоже!) уже требуется несколько тактов. И чем дальше, тем больше разница в скорости. А с регистрами процессор работает за 1 такт. Виртуальные стековые машины хорошо бы ложились на реальные стековые машины. Но таковых нет, а на реальные регистровые машины они ложатся хуже. Были интересные проекты стековых процессоров, где вершина стека хранилась бы не в памяти, а в регистрах. Т.е. обращение к стеку делалось бы уже за 1 такт. Но сколько перспективных архитектур проиграло конкуренцию x86 и ARM…

    Коль автор так глубоко копнул в прошлое, то для восстановления исторический справедливости хотелось бы заметить, что в СССР тоже были подобные технологии.

    • Язык программирования Алмо
    • Система программирования Сигма
    • Универсальный машинно-ориентированный язык программирования Эпсилон
    • Система программирования Бета


    Какие-то из них были «универсальным ассемблером», другие – системой, частью которой являлось универсальное промежуточное представление. Подробнее: Экскурс в историю разработок языков программирования и компиляторов в СССР


    1. tyomitch
      22.11.2015 19:02
      +2

      Спасибо за подробный комментарий!

      Дело в том, что стек – это область памяти, которую адресуют регистры процессора.
      Это не обязательно так; стек может частично (как в Itanium) или полностью (как в 80x87) быть внутри процессора.

      Успех/неуспех процессоров, да и любых технологических новшеств, далеко не всегда определяется их техническими характеристиками.


      1. Flammar
        25.11.2015 11:05
        +1

        Это не обязательно так; стек может частично (как в Itanium) или полностью (как в 80x87) быть внутри процессора.
        Тут получается «вилка»: либо мы не используем, грубо говоря, половину «регистров процессора» (= быстрой дорогой внутрипроцессорной памяти, отведённой под стек) только из-за того, что стек дотуда «не дорос», либо перед каждой операцией что-то подгружаем из ОЗУ и после каждой операции что-то выгружаем в ОЗУ. Первое расточительно и из-за этого медленно, второе просто медленно.


        1. tyomitch
          26.11.2015 04:11
          +3

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


          1. Flammar
            26.11.2015 12:45
            -1

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


            1. konsoletyper
              26.11.2015 13:59
              +1

              При традиционной архитектуре мы можем когда угодно использовать какие угодно регистры. При стековой — только те, до которых дорос стек, и то только на хранение, а на запись — только верхушку стека.

              Не знаю, как в FORTH, а в JVM, которая стековая, помимо стека есть ещё и так называемые локальные переменные (по сути — регистры), есть операция swap, есть серия операция dupx. Думаю, непрактично делать пуристическую машину, работающую строго на одном принципе.

              Кроме того, тут проблема ещё и в том, что работаем мы пусть и с вершиной стека, но хранить-то нужно весь стек! Кстати, эта проблема имеет аналог в регистровых машинах — в них оптимизаторы стараются как можно ближе «сдвинуть» места определения переменной (def) и места её использования (use).

              Про «скользящие» регистры — идея интересная, не помню, чтоб была реализована аппаратно, но, может быть, кем-то когда-то воплощено, просто малоизвестно.

              Не уверен, что правильно уловил суть высказывания. Если имелся в виду маппинг большого числа регистров виртуальной машины на малое число регистров реальной — то аппаратная реализация не нужна. Если всё же про маппинг стека, то это сводится к предыдущей задаче, потому что в ряде случаев стековую машину можно легко сконвертировать в регистровую (по крайней мере, это верно для байт-кода Java и MSIL).


              1. Flammar
                26.11.2015 15:30

                Если что, я писал не про виртуальные машины, а про аппаратные реализации.

                Не знаю, как в FORTH, а в JVM, которая стековая, помимо стека есть ещё и так называемые локальные переменные (по сути — регистры)
                Локальные переменные используются меньше, чем стек, нерационально использовать под них более быструю память, чем под стек.

                В FORTH, насколько я помню из книжек 20-летней давности, под каждую переменную резервируется место в словаре, и она глобальная. И стек там не помню чтоб по кадрам делился как в JVM.
                Не уверен, что правильно уловил суть высказывания.
                Идея была про чисто аппаратную реализацию «прозрачного» маппинга верхушки стека на регистры при том, что глубина стека может быть какой угодно большой и располагаться он может где угодно — в регистрах, в кэше или в ОЗУ, или распределён по всем этим трём областям памяти.


            1. tyomitch
              26.11.2015 19:44
              +1

              При стековой — только те, до которых дорос стек

              Если стек «не дорос», значит код работает с небольшим числом значений. Естественно, что при этом использоваться будет только небольшая часть аппаратных регистров — хоть при стековой архитектуре, хоть при традиционной.

              Про «скользящие» регистры — идея интересная, не помню, чтоб была реализована аппаратно

              Itanium же.


    1. konsoletyper
      23.11.2015 11:31
      +4

      Хотелось бы дополнить автора в вопросе, почему стек-ориентированные виртуальные машины проигрывают регистровым. Потому что они медленнее. Дело в том, что стек – это область памяти, которую адресуют регистры процессора. Чтение из стека или запись в него – это работа с памятью.

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

      Однако, серьёзные виртуальные машины занимаются вещами более серьёзными, чем интерпретация. Обычно, они либо в режиме AOT, либо JIT, генерируют машинный код для целевой архитектуры. И вот тут дискуссия переходит в другую плоскость. Во-первых, достаточно умному бэкэнду, что ему дали — регистровое представление или стековое, в итоге он сгенерит одинаково быстрый код. Если ему не нравится одно, всегда можно сконвертировать его в другое. Во-вторых, нас начинает волновать производительность компилятора, а вот тут уже не всё однозначно. С точки зрения бэкэнда и то и другое одинаково плохо, а хорошо — это умные слова вроде SSA и PDG, но они уже гораздо меньше похожи как на регистровые, так и на стековые машины. В этом смысле шустрее всего должен быть LLVM. К слову, та же JVM внутри себя генерит SSA-представление из байткода и в дальнейшем работает только с ним, т.е. выполняет ту работу, которую за LLVM делает фронтэнд. Вопрос о том, на кого переложить больший объём работы (компилятор или виртуальную машину), зависит от целей проекта и требований к нему. Я так понимаю, LLVM проектировался с целью достичь быстрой генерации машинного кода, а JVM — с целью упростить работу создателям компилятора.


      1. tyomitch
        23.11.2015 13:03

        Я так понимаю, LLVM проектировался с целью достичь быстрой генерации машинного кода, а JVM — с целью упростить работу создателям компилятора.

        Я понял чуть иначе: что среди целей Java-байткода были компактное представление и возможность эффективной интерпретации (без AOT и JIT) на маломощных платформах. А перед LLVM-IR таких целей не стояло: только эффективная компиляция, и ничего другого.


  1. yomayo
    23.11.2015 16:48
    +3

    Знакомясь с LLVM IR, я обратил внимание, что LLVM IR имеет скудные средства для работы с механизмом стека. Если x86 содержит в себе достаточно полный набор инструкций для манипуляций со стеком, то LLVM IR ближе к языкам типа C и я не увидел в нём операций явного изменения указателя стека.

    Между тем, Есть языки, которые активно используют стек. Самый известный их представитель – это язык FORTH. В программах на FORTH очень часто пишут «PUSH» и «POP». Было бы интересно посмотреть реализацию FORTH на LLVM.

    Существует проект, который поставил своей целью преобразовывать код x86 в LLVM. Называется он McSema, финансируется DARPA. Задумка хорошая: всё, что нажито непосильным трудом для x86, сделать доступным на других платформах с помощью LLVM. Но как в LLVM так же манипулировать стеком, как это делает x86? Конечно, LLVM IR, будучи Тьюринг-полным языком, может решить любые задачи. Но преобразование x86 -> LLVM IR -> x86 дало бы в некоторых случаях, как мне кажется, к немалому увеличению кода на выходе. Будет ли программа на FORTH после преобразования x86 -> LLVM IR -> x86 столь же лаконична, как ранее?

    Я задал эти вопросы Andrew Ruef, соавтору проекта. На это он мне ответил:

    The way we do this in LLVM is by representing the machine state as a structure with registers as field members. For example:

    struct RegisterState {
      uint32_t eax;
      uint32_t ebx;
      …
      uint32_t esp;
    }; 
    

    So each translation routine is a function that produces a transformation on struct RegisterState. This is what xchg esp, eax would look like:

    void doXchg_ESP_EAX(struct RegisterState *state) {
      uint32_t tmp = state->eax;
      state->eax = state->esp;
      state->esp = tmp;
      return;
    }
    

    We use LLVM as a language to express the effects of instruction on a machine state. We model the machine state as an object in LLVM. We do this for registers and memory. The model doesn’t have a special type of memory, which matches the x86 model where there is no distinction between global memory, heap memory, and stack memory in the address space.


    1. tyomitch
      23.11.2015 17:42
      -1

      Между тем, Есть языки, которые активно используют стек. Самый известный их представитель – это язык FORTH.
      Ну и где сейчас FORTH?

      Но преобразование x86 -> LLVM IR -> x86 дало бы в некоторых случаях, как мне кажется, к немалому увеличению кода на выходе. Будет ли программа на FORTH после преобразования x86 -> LLVM IR -> x86 столь же лаконична, как ранее?
      Конечно, нет. А зачем?

      Если вас устраивает имеющийся код для x86, зачем его перегонять в LLVM-IR и обратно?

      Например, в LLVM встроен бэкенд, генерирующий из LLVM-IR код на Си. Но никто не ожидает, что компиляция Си -> LLVM-IR -> Си произведёт такой же лаконичный код, каким был исходный. Кому нужно перегонять код на Си в код на Си?


      1. yomayo
        23.11.2015 18:01

        По поводу x86 -> LLVM IR -> x86. Есть немало унаследованного 16-битного кода, который не работает на 64-битных машинах. Есть у меня одна такая программка: нужная, но исходников к ней нет. Если преобразовать её с помощью McSema в 32-битный код x86, то было бы здорово.

        FORTH как таковой меня не интересует. Я его привёл лишь как пример. Но меня несколько разочаровывает LLVM тем, что в нём мало возможностей для манипуляций со стеком. По этой причине я не могу его использовать в одном проекте. А жаль.


        1. tyomitch
          23.11.2015 18:37

          Есть немало унаследованного 16-битного кода, который не работает на 64-битных машинах. Есть у меня одна такая программка: нужная, но исходников к ней нет. Если преобразовать её с помощью McSema в 32-битный код x86, то было бы здорово.

          Насколько я понимаю, для такого преобразования достаточно дизассемблировать старый код и сассемблировать его 32-битным ассемблером; и McSema ни к чему.


          1. yomayo
            23.11.2015 18:46

            достаточно дизассемблировать старый код и сассемблировать его 32-битным ассемблером
            Значит, у меня не было сильного непреодолимого желания :)


      1. chaetal
        24.11.2015 11:01
        -2

        Ну и где сейчас FORTH?


        Может быть, правильнее спросить, где сейчас мы?


    1. konsoletyper
      24.11.2015 11:56
      +1

      Между тем, Есть языки, которые активно используют стек. Самый известный их представитель – это язык FORTH. В программах на FORTH очень часто пишут «PUSH» и «POP». Было бы интересно посмотреть реализацию FORTH на LLVM.

      Как ни странно, инженерный гений способен разруливать подобные ситуации выдавать изящные и красивые решения подобных задач. Например, JavaScript, который весь такой динамический и с прототипным ООП, смогли-таки эффективно исполнять на железе с «традиционной» архитектурой (на самом деле, это смогли ещё для self, с которого перенесли опыт). Кстати, вроде бы постепенно в V8 выбрасывают crankshaft и внедряют LLVM, хотя я не очень пристально слежу за последними изменениями. Ну да, для реализации прототипов замутили скрытые классы, а для генерации быстрого кода делают speculative optimizations и OSR. Уверен, подобные финты ушами, если очень нужно будет, смогут придумать, если это будет экономически оправданно, и для Lisp, и для Self.

      Кстати, ещё один пример, когда совсем чуждую концепцию умудряются эффективно реализовать на традиционном железе — это чистые функциональные языки вроде Haskell. Там концепция ещё более чуждая, чем Self/JavaScript, и ничего, придумывают всякие страшные штуки вроде spineless tagless g-machine и т.д.


    1. potan
      24.11.2015 14:15
      +1

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


    1. potan
      24.11.2015 14:19
      +1

      Мне интереснее, можно ли реализовать LLVM для Intel iAPX 432. Там нет операций работы с указателями, обращаться можно только по индексу в массиве. А LLVM часто получает непосредственный указатель на элемент массива или структуры.


  1. potan
    24.11.2015 13:59

    Есть еще интересный универсальный код в системе AS/400, которая была реализована на процессорах с различной системой команд (правда одной фирмы и со специальной аппаратной поддержкой).


    1. tyomitch
      24.11.2015 14:59

      Хотелось бы про него узнать хоть какие-нибудь подробности.

      К сожалению, гугл не обнаруживает никакой информации, кроме того, что «в системе AS/400 был интересный универсальный код».