Вольный перевод статьи Fedor Indutny "SMIs and Doubles"
SMI и Double представления
Цели
В прошлой статье мы реализовали простой распределитель памяти и научили наш компилятор работать с числами с плавающей запятой, храня их в выделенных объектах кучи. Однако числа с плавающей запятой не подходят для некоторых точных операций, а также, поскольку они хранятся в памяти, что требует дополнительной загрузки и сохранения памяти, что снижает производительность кода.
Обе проблемы можно решить с помощью поддержки целых чисел, как мы делали в первой статье, что значит что нам нужно внедрить поддержку обоих типов чисел в рантайме компилятора.
Отметки
Вспомним, что мы храним указатели и числа в виде 64-х битных регистров общего назначения (rax
, rbx
, ...). Главная проблема в том, что мы должны иметь возможность определить, полученный регистр (например, rax
) имеет указатель на объект кучи или на само число (SMI).
Обычно для такого используется метод "tagging". Есть несколько способов реализовать отметки, включая: Nan-Boxing (описание находится в блоке Mozilla’s New JavaScript Value Representation) и Nun-Boxing. Нашему компилятору нужно будет просто зарезервировать один бит 64-х битного регистра и добавлять к нему 1
, если значение является указателем, и 0
, если значение является SMI (Small Integer).
Пример:
Заметьте что чотбы получить значение SMI нам потребуется сдвинуть его на один бит вправо (>> 1
) и конвертировать целое число в SMI - сдвиг влево (<< 1
). Использование нуля для отметки SMI очень окупается, поскольку нам не нужно размечать числа, чтобы выполнить сложение и вычитание.
Чтобы использовать отмеченные указатели нам нужно найти значение находящееся на один бит левее фактического, что достаточно легко реализовать:
// Предположим что отмеченный указатель находится в rbx
// И нам нужно загрузить его содержимое в rax
this.mov('rax', ['rbx', -1]);
И для удобства, пример непомеченных SMI:
// Непомеченный
this.shr('rax', 1);
// Помеченный
this.shl('rax', 1);
И одна из главных частей - проверка является ли значение указателем:
// Проверить, есть ли у'rax' последний бит
this.test('rax', 1);
// 'z' означает zero
// Перейти к метке, если `(rax & 1) == 0`
this.j('z', 'is-smi');
// 'nz' означает non-zero
// Перейти к метке, если `(rax & 1) != 0`
this.j('ne', 'is-heap-object-pointer');
Перерабатываем имеющийся код
С помощью кода из прошлого поста, мы наконец можем приступить к реализации всех новых штук.
В первую очередь, давайте добавим helper методы.
function untagSmi(reg) {
this.shr(reg, 1);
};
function checkSmi(value, t, f) {
// Если не были определены `true-` и `false-`
// просто тестируем значение.
if (!t && !f)
return this.test(value, 1);
// Вводим новую область видимости, чтобы иметь возможность использовать именованные метки
this.labelScope(function() {
// Тестируем значение
this.test(value, 1);
// Пропускаем SMI случай, если результат non-zero
this.j('nz', 'non-smi');
// Запускаем SMI случай
t.call(this);
// Переходим к общему концу
this.j('end');
// Запускаем Non-SMI случай
this.bind('non-smi');
f.call(this);
// Общий конец
this.bind('end');
});
};
function heapOffset(reg, offset) {
// ПРИМЕЧАНИЕ: 8 - это размер указателя x64 архитектуры.
// Добавляем 1 к смещению, поскольку на первом месте
// хранится тип объектов кучи.
return [reg, 8 * ((offset | 0) + 1) - 1];
};
Мы можем включить эти методы в контекст jit.js, передав их как свойство helpers
в API метод jit.compile()
:
var helpers = {
untagSmi: untagSmi,
checkSmi: checkSmi,
heapOffset: heapOffset
};
jit.compile(function() {
// Здесь мы можем использовать хелперы:
this.untagSmi('rax');
this.checkSmi('rbx', function() {
// Здесь работаем со SMI
}, function() {
// Здесь работаем с указателями
});
this.mov(this.heapOffset('rbx', 0), 1);
}, { stubs: stubs, helpers: helpers });
Назначение
Сейчас нам нужно сделать чтобы наш стаб Alloc
вовзращал помеченный указатель. Также мы используем возможность и немного улучшим его с помощью добавления аргументов tag
и size
:
stubs.define('Alloc', function(size, tag) {
// Сохраняем регистры 'rbx' и 'rcx'
this.spill(['rbx', 'rcx'], function() {
// Загружаем `offset`
//
// ПРИМЕЧАНИЕ: Мы будем использовать указатель для переменной `offset`, чтобы была вохможность
// позже обновить ее
this.mov('rax', this.ptr(offset));
this.mov('rax', ['rax']);
// Конец загрузки
//
// ПРИМЕЧАНИЕ: То же самое относится и к концу страницы, но мы не обновляем его прямо сейчас
this.mov('rbx', this.ptr(end));
this.mov('rbx', ['rbx']);
// Вычисляем новый `offset`
this.mov('rcx', 'rax');
// Добавляем размер тега и тела
this.add('rcx', 8);
this.add('rcx', size);
// Проверяем, не переполняется ли буффер фиксированного размера
this.cmp('rcx', 'rbx');
// this.j() выполняет условный переход к указанной метке..
// 'g' означает 'greater'
// 'overflow' - это имя метки, связанной ниже
this.j('g', 'overflow');
// Окей, может двигаться дальше и обновить offset
this.mov('rbx', this.ptr(offset));
this.mov(['rbx'], 'rcx');
// Первый 64-х битный указатель зарезервирован для 'tag',
// второй имеет значение `double`
this.mov('rcx', tag);
this.mov(['rax'], 'rcx');
// !!!!!!!!!!!!!!!!!!!
// ! Указатель метки !
// !!!!!!!!!!!!!!!!!!!
this.or('rax', 1);
// Возвращаем 'rax'
this.Return();
// Переполнение :(
this.bind('overflow')
// Вызываем javascript функцию!
// ПРИМЕЧАНИЕ: Штука ниже очень прикольная, но я поясню ее позже
this.runtime(function() {
console.log('GC is needed, but not implemented');
});
// Краш
this.int3();
this.Return();
});
});
Математические стабы
Также нам придется немного доработать математические операции для поддержки SMI и double значений, давайте пока разделим их и добавим код который обрабатывает double:
var operators = ['+', '-', '*', '/'];
var map = { '+': 'addsd', '-': 'subsd', '*': 'mulsd',
'/': 'divsd' };
// Определяем `Binary+`, `Binary-`, `Binary*` и `Binary/` стабы
operators.forEach(function(operator) {
stubs.define('Binary' + operator, function(left, right) {
// Сохраняем 'rbx' и 'rcx'
this.spill(['rbx', 'rcx'], function() {
// Загружаем аргументы для rax и rbx
this.mov('rax', left);
this.mov('rbx', right);
// Конвертируем оба числа в double
[['rax', 'xmm1'], ['rbx', 'xmm2']].forEach(function(regs) {
var nonSmi = this.label();
var done = this.label();
this.checkSmi(regs[0]);
this.j('nz', nonSmi);
// Конвертируем целое число в double
this.untagSmi(regs[0]);
this.cvtsi2sd(regs[1], regs[0]);
this.j(done);
this.bind(nonSmi);
this.movq(regs[1], this.heapOffset(regs[0], 0));
this.bind(done);
}, this);
var instr = map[operator];
// Выполняем бинарную операцию
if (instr) {
this[instr]('xmm1', 'xmm2');
} else {
throw new Error('Unsupported binary operator: ' +
operator);
}
// Выделяем новое число и складываем в него значение
// ПРИМЕЧАНИЕ: Последние два аргумента являются
// аргументами стаба (`size` и `tag`)
this.stub('rax', 'Alloc', 8, 1);
this.movq(this.heapOffset('rax', 0), 'xmm1');
});
this.Return();
});
});
Обратите внимание, что стаб также преобразует все входящие числа в double.
Компилятор
И сснова к коду компилятора:
function visitProgram(ast) {
assert.equal(ast.body.length,
1,
'Only one statement programs are supported');
assert.equal(ast.body[0].type, 'ExpressionStatement');
// Конвертируем в целое число имеющийся в 'rax' указатель
visit.call(this, ast.body[0].expression);
// Получаем число с плавающей запятой из числа кучи
this.checkSmi('rax', function() {
// Убираем метку smi
this.untagSmi('rax');
}, function() {
this.movq('xmm1', this.heapOffset('rax', 0));
// Округляем до нуля
this.roundsd('zero', 'xmm1', 'xmm1');
// Конвертируем double в целое число
this.cvtsd2si('rax', 'xmm1');
});
}
function visitLiteral(ast) {
assert.equal(typeof ast.value, 'number');
if ((ast.value | 0) === ast.value) {
// SMI, отмеченное значение
// (т.е. val * 2) с последним битом, приведенным к нулю
this.mov('rax', utils.tagSmi(ast.value));
} else {
// Выделяем новое число кучи
this.stub('rax', 'Alloc', 8, 1);
// Сохраняем 'rbx' регистр
this.spill('rbx', function() {
this.loadDouble('rbx', ast.value);
// ПРИМЕЧАНИЕ: Последний бит указателей приведен к единице
// Поэтому нам нужно использовать процедуру 'heapOffset'
// чтобы получить доступ к памяти
this.mov(this.heapOffset('rax', 0), 'rbx');
});
}
}
function visitBinary(ast) {
// Сохраняем 'rbx' после того, как вышли из AST узла
this.spill('rbx', function() {
// Посещаем правую сторону выражения
visit.call(this, ast.right);
// Перемещаем в 'rbx'
this.mov('rbx', 'rax');
// Посещаем левую сторону выражения (результат в 'rax')
visit.call(this, ast.left);
//
// Обобщяя, левая сторона хранится в 'rax', правая - в 'rbx'
//
if (ast.operator === '/') {
// Вызываем стаб для операции деления
this.stub('rax', 'Binary' + ast.operator, 'rax', 'rbx');
} else {
this.labelScope(function() {
// Проверяем, являются ли оба числа SMI
this.checkSmi('rax');
this.j('nz', 'call stub');
this.checkSmi('rbx');
this.j('nz', 'call stub');
// Сохраняем rax в случае смещения
this.mov('rcx', 'rax');
// ПРИМЕЧАНИЕ: В этой точке оба 'rax'и 'rbx' являются отмеченными.
// Метки не нужно убирать, если мы выполняем операции
// сложения и вычитания. Однако, в случае с
// умножением, результат будет вдвое больше, если
// не убрать метки.
if (ast.operator === '+') {
this.add('rax', 'rbx');
} else if (ast.operator === '-') {
this.sub('rax', 'rbx');
} else if (ast.operator === '*') {
this.untagSmi('rax');
this.mul('rbx');
}
// При переполнении восстановить 'rax' из 'rcx' и вызвать стаб
this.j('o', 'restore');
// Иначе вернуть'rax'
this.j('done');
this.bind('restore');
this.mov('rax', 'rcx');
this.bind('call stub');
// Вызвать стаб и вернуть номер кучи в 'rax'
this.stub('rax', 'Binary' + ast.operator, 'rax', 'rbx');
this.bind('done');
});
}
});
}
function visitUnary(ast) {
if (ast.operator === '-') {
// Делаем аргумент отрицательным эмулируя бинарное выражение
visit.call(this, {
type: 'BinaryExpression',
operator: '*',
left: ast.argument,
right: { type: 'Literal', value: -1 }
})
} else {
throw new Error('Unsupported unary operator: ' + ast.operator);
}
}
Подведем итоги: теперь мы можем работать с SMI значениями по умолчанию, внедряя ради скорости дополнительные операции, и возвращаться к double значениям в случае переполнения или любых других проблем, вроде попытки найти сумму значений double и SMI!
Top comments (0)