Прошлое
Повествование можно начать с 1962 г., когда в Кембриджском университете началась работа над CPL («Cambridge Programming Language») — «усовершенствованным вариантом» ALGOL-60. К работе над языком подключился аспирант Мартин Ричардс; главной сложностью в реализации нового ЯП ему показалась необходимость ручного портирования компилятора для разных компьютерных платформ. В частности, когда кембриджский EDSAC-2 заменили на Atlas-2, разработчики CPL потратили много времени на портирование своего компилятора для новой платформы.
Диссертация Мартина была посвящена «само-компилирующемуся» CPL: разработанный Мартином компилятор был написан на сильно упрощённом варианте CPL, компилятор которого несложно было написать на тогдашнем макроассемблере. Перенос CPL на новую платформу теперь можно было выполнить в два шага:
- Вручную пишем компилятор «упрощённого CPL»;
- Компилируем им компилятор «полного CPL».
На этом Мартин не остановился, и разработал BCPL — систему для разработки переносимых компиляторов. Компилятор BCPL генерировал псевдокод, названный Мартином «OCODE».
OCODE | «расшифровка» («procode») | |
---|---|---|
94 5 L1 83 73 69 86 69 95 4 42 0 42 0 40 2 14 83 42 0 42 1 40 2 14 83 42 2 40 3 42 1 15 92 85 L5 90 L6 42 1 40 4 40 2 14 83 40 4 42 1 14 80 4 90 5 40 4 40 5 88 L6 91 4 42 2 40 3 42 1 15 92 85 L7 90 L8 40 4 40 2 14 8 87 L9 40 4 42 2 11 92 85 L11 90 L10 42 0 40 6 40 2 14 83 40 4 40 6 14 80 6 90 L11 40 6 40 3 22 86 L10 91 6 90 L9 40 4 42 1 14 80 4 90 L7 40 4 40 5 88 L8 91 4 97 103 0 |
ENTRY 5 L1 'S' 'I' 'E' 'V' 'E' SAVE 4 LN 0 LN 0 LP 2 PLUS STIND LN 0 LN 1 LP 2 PLUS STIND LN 2 LP 3 LN 1 MINUS STORE JUMP L5 LAB L6 LN 1 LP 4 LP 2 PLUS STIND LP 4 LN 1 PLUS SP 4 LAB L5 LP 4 LP 5 ENDFOR L6 STACK 4 LN 2 LP 3 LN 1 MINUS STORE JUMP L7 LAB L8 LP 4 LP 2 PLUS RV JF L9 LP 4 LN 2 MULT STORE JUMP L11 LAB L10 LN 0 LP 6 LP 2 PLUS STIND LP 4 LP 6 PLUS SP 6 LAB L11 LP 6 LP 3 LS JT L10 STACK 6 LAB L9 LP 4 LN 1 PLUS SP 4 LAB L7 LP 4 LP 5 ENDFOR L8 STACK 4 RTRN ENDPROC 0 |
; стековый кадр (два параметра и две локальные переменные) ; поместить на стек число 0 ; поместить ещё один 0, прибавить к нему 2-ой элемент стека ; записать в массив на вершине стека значение под ним ; всё то же самое для 1-ого элемента массива ; поместить на стек число 2 ; вычесть единицу из значения 3-его элемента стека ; записать результат в локальную переменную ; перейти к метке L5 ; объявление метки L6 ; взять 4-ый элемент стека, записать в массив по этому индексу 1 ; прибавить к 4-ому элементу стека 1, записать результат обратно ; L5: перейти к метке L6, если 4-ый элемент стека <= 5-ому ; объявление, что на стеке сейчас четыре элемента ; вычесть единицу из значения 3-его элемента стека ; перейти к метке L7 ; L8: сложить 4-ый и 2-ой элементы стека ; прочитать значение по этому адресу; если это 0, перейти к L9 ; умножить 4-ый элемент на два ; перейти к метке L11 ; объявление метки L10 ; взять 6-ой элемент стека, записать в массив по этому индексу 0 ; прибавить к 6-ому элементу стека 4-ый, записать рез-т обратно ; объявление метки L11 ; перейти к метке L10, если 7-ой элемент стека меньше 4-ого ; на стеке сейчас шесть элементов; объявление метки L9 ; прибавить к 4-ому элементу стека 1, записать результат обратно ; L10: перейти к L8, если 4-ый элемент стека <= 5-ому ; на стеке четыре элемента; окончание процедуры |
Исходный код на BCPL:
В более новых версиях OCODE добавилась поддержка чисел с плавающей точкой (соответственно, набор поддерживаемых опкодов почти удвоился), а также удалили опкодLET sieve(workvec, vecsize) BE { workvec!0 := 0 workvec!1 := 0 FOR i = 2 TO vecsize-1 DO workvec!i := 1 FOR i = 2 TO vecsize-1 DO IF workvec!i DO { LET j = 2 * i WHILE j < vecsize DO { workvec!j := 0 j := j + i } } }
ENDFOR
— вместо него генерируется пара LE JT
.
Среди «универсальных машинных языков» OCODE уникален тем, что метки в нём определяются специальными инструкциями — т.е. для интерпретации программы её нужно сначала всю загрузить в память, и найти в ней метки.
Компилятор BCPL(1) поставлялся в виде OCODE, и чтобы перенести его на новую платформу, нужно было:
- Вручную написать интерпретатор псевдокода(2) (на любом языке, хоть на Бейсике);
- Адаптировать кодогенератор,(3) написанный на BCPL, для своей платформы;
- Запустить под интерпретатором (2) компилятор BCPL (1), скормить ему кодогенератор (3), и получить на выходе исполнимый файл кодогенератора(4);
- Интерпретатор (2) нам с этого момента больше не нужен.
- Прогнать через кодогенератор (4) псевдокод компилятора (1), и получить на выходе исполнимый файл компилятора.
Такой подход означал, что для переноса компилятора на новую платформу требуется лишь самый минимум низкоуровневого программирования; и действительно, реализация BCPL была завершена к 1967 г. — раньше, чем была завершена реализация CPL, начатая на несколько лет раньше!
Достоинства BCPL применительно к системному программированию вдохновили Кена Томпсона на создание языка Би, а тот — коллегу Кена, Денниса Ритчи, на создание Си. Именно из BCPL пошла традиция обозначать {
фигурными скобками}
блоки программы, и именно на BCPL была написана первая программа «Hello, World!».
Более важная нам причина, по которой BCPL вошёл в историю: OCODE — первая универсальная «архитектура набора команд» (ISA), т.е. «виртуальная машина», не привязанная ни к какой конкретной аппаратной платформе с её особенностями. BCPL, таким образом — первый язык программирования, соответствующий парадигме «Write once, run anywhere» (WORA): программу на BCPL можно распространять в скомпилированном виде, и её можно будет запустить на любой платформе, для которой существует OCODE-кодогенератор.GET "libhdr" LET start() = VALOF { writef("Hello*n") RESULTIS 0 }
Сам BCPL и его OCODE так и не завоевали популярности за пределами Соединённого Королевства, но идея WORA прижилась. В 1974 г. уже на Континенте, в ETH Zürich, под руководством Никлауса Вирта был разработан «Pascal-P» — с компиляцией в промежуточный «p-код», и затем компиляцией этого «p-кода» в исполнимый код для конкретной аппаратной платформы. На основе Pascal-P уже по другую сторону океана, в UCSD, была создана ОС p-System (1978), где кодогенератора не было вообще: паскалевский p-код сам являлся целевой платформой компилятора, а ядро ОС фактически было интерпретатором p-кода, работающим «на голом железе». Все системные утилиты p-System поставлялись в виде кроссплатформенного p-кода, и для адаптации p-System к новой аппаратной платформе достаточно было написать для неё интерпретатор p-кода — перекомпилировать нечего!
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 ; записать значение на вершине стека в стековый кадр (в локальную переменную) ; поместить на стек локальную переменную
|
Исходный код на Паскале:
По сравнению с OCODE, который поддерживал работу только с целыми и с плавающими числами размером в машинное слово, p-код Паскаля поддерживает куда более разнообразные типы данных — строки, массивы, «упакованные массивы» (с размером элемента менее чем в одно слово), записи, множества и т.д.; а также динамическое выделение памяти. Другое существенное различие в том, что «рабочий стек» процедуры и «стековый кадр» с параметрами и локальными переменными теперь разделены.const data_size = 50; type data_array = array[1..data_size] of boolean; procedure sieve(var workvec: data_array); var i, j: integer; begin workvec[1] := false; for i := 2 to data_size-1 do workvec[i] := true; for i := 2 to data_size-1 do if workvec[i] then begin j := 2 * i; while j < data_size do begin workvec[j] := false; j := j + i; end end end;
Для компактности p-кода у многих инструкций есть «сокращённые опкоды» для небольших, часто используемых значений аргументов.
p-System оказалась достаточно популярной: несколько моделей Apple II и несколько компьютеров от IBM, в том числе оригинальный IBM PC (1981), предлагались с p-System в качестве ОС; а начиная с 1979 г., Western Digital выпускала миникомпьютеры Pascal MicroEngine, в которых выполнение p-кода было реализовано аппаратно. Таким образом p-код из абстрактного «универсального машинного языка» превратился в настоящий машинный язык настоящего компьютера.
Хотя к девяностым p-System уже исчезла с рынка, она успела вдохновить Джеймса Гослинга на создание в 1996 г. виртуальной машины Java. «Write once, run anywhere!» превратилось в раскрученный рекламный слоган, а реализации «универсального машинного языка» («байткода») развивались параллельно в двух направлениях:
- Ускорение программной реализации (JIT-компиляция в JDK 1.1, 1997 г.)
- Попытки аппаратной реализации:
Неудивительно, что ни один из «байткод-процессоров» — ни для p-кода, ни для Java — не стал коммерчески успешным. (Сюда же можно отнести и намного более ранний процессор Intel iAPX 432 (1981) — аппаратную реализацию байткода для Ады.) Байткод Java, как и p-код Вирта, как и OCODE — все они стек-ориентированные, потому что именно такой промежуточный код проще всего генерировать и проще всего интерпретировать. Стек «виртуальной машины» не ограничен; у «железного» процессора, напротив, фиксированное число регистров, и одна из самых важных задач компилятора — распределить данные по регистрам так, чтобы обращения к памяти выполнялись как можно реже. Для того, чтобы «в железе» отслеживать зависимости между данными, распределять их по «железным» регистрам, и переставлять обращения к ним так, чтобы уложиться в «железные» ограничения — требуются очень сложные механизмы. Получается, что при одном и том же уровне полупроводниковых технологий эффективнее создать процессор для простой ISA, и на нём реализовать трансляцию байткода, — чем выполнять этот же байткод «в железе». Вновь и вновь мечтательные айти-предприниматели убеждались, что превратить «универсальный машинный язык» в настоящий — хоть и возможно технически, но коммерчески бесперспективно.
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.
Машинный код | Ассемблер |
---|---|
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 наравне с машинным кодом.
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-овских платформ», и никаких других.
.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-форма»): каждой переменной значение присваивается единожды, и во время работы программы оно не меняется. Такая форма упрощает отслеживание зависимостей между данными, упрощает перестановку и замену отдельных инструкций, обнаружение и удаление повторяющихся или «мёртвых» инструкций, и т.д.
; 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";
, он выполняется «по-старинке».
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; а его старые версии браузеров уже умеют выполнять.
_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.
Посмотрим.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.
Комментариев нет:
Отправить комментарий