Недавно я улучшил поддержку 64-битных целых чисел в Cheerp, компиляторе C / C ++ в WebAssembly / JavaScript.

В этом посте я объясню, почему поддержка 64-битных целых чисел требует особого внимания, что Cheerp делал в прошлом, почему текущая ситуация лучше и что мы сможем сделать в будущем для дальнейшего улучшения поддержки.

О Cheerp

Cheerp - это компилятор C / C ++ в WebAssembly / JavaScript, аналогичный Emscripten. Его основное внимание уделяется лучшей совместимости с API-интерфейсами браузера и сторонними библиотеками JavaScript. Это достигается путем компиляции кода либо в WebAssembly, используя стандартную плоскую модель памяти, либо в JavaScript, используя модели объектной памяти, которые сопоставляют экземпляры структур / классов C ++ с объектами JavaScript, собирающими мусор.

Поддержка 64-битных целых чисел в JavaScript

До появления WebAssembly Cheerp поддерживал компиляцию кода C ++ в простой JavaScript. JavaScript не имеет встроенной поддержки 64-битных целых чисел (ну, сейчас есть BigInt… подробнее об этом позже), поэтому нам нужен был способ поддержки этих целых чисел и их операций в терминах более простых типов.

Несмотря на то, что JavaScript номинально поддерживает только числа с плавающей запятой двойной точности, мы можем эффективно и точно представлять 32-битные целые числа. Затем мы можем разложить одно 64-битное значение как массив из двух 32-битных значений и явно реализовать все операции в терминах старших и младших 32 бит.

Это может быть не очень эффективно, но позволяет компилировать программы, использующие такие типы, как int64_t (он же long long) и uint64_t (он же unsigned long long).

Clang обычно компилирует эти типы C ++ в тип LLVM i64. Но поскольку мы не можем представить i64 в JavaScript, мы модифицировали Clang для компиляции операций на int64_t/uint64_t в терминах 32-битных инструкций для i32type. Это разложение было выполнено непосредственно в Clang при генерации кода LLVM IR. Причина этого заключалась в том, чтобы выполнить эту операцию как можно скорее, чтобы обеспечить больше оптимизаций, и никогда не иметь i64 значений в нашем IR, чтобы избежать возможных проблем.

Например, для следующего кода C:

int64_t i = ...;
int64_t j = ...;
j = j & i;

Clang обычно излучает LLVM IR следующим образом:

%i.addr = alloca i64, align 8
%j.addr = alloca i64, align 8
[...]
%i = load i64, i64* %i.addr, align 8
%j = load i64, i64* %j.addr, align 8
%and = and i64 %i, %j

store i64 %and, i64* %j.addr, align 8

В Cheerp он выдавал что-то вроде этого:

%i.addr = alloca [2 x i32], align 8
%j.addr = alloca [2 x i32], align 8
[...]
%i.gep.high = getelementptr inbounds [2 x i32], [2 x i32]* %i.addr, i32 0, i32 1
%i.high = load i32, i32* %i.gep.high, align 4
%i.gep.low = getelementptr inbounds [2 x i32], [2 x i32]* %i.addr, i32 0, i32 0
%i.low = load i32, i32* %i.gep.low, align 8
%j.gep.high = getelementptr inbounds [2 x i32], [2 x i32]* %j.addr, i32 0, i32 1
%j.high = load i32, i32* %j.gep.high, align 4
%j.gep.low = getelementptr inbounds [2 x i32], [2 x i32]* %j.addr, i32 0, i32 0
%j.low = load i32, i32* %j.gep.low, align 8
%and.high = and i32 %i.high, %j.high
%and.low = and i32 %i.low, %j.low

store i32 %and.high, i32* %j.gep.high, align 8
store i32 %and.low, i32* %j.gep.low, align 4

Это дизайнерское решение оказалось препятствием для поддержки реальных 64-битных целых чисел для вывода WebAssembly.

Поддержка 64-битных целых чисел в WebAssembly

WebAssembly изначально поддерживает значения i64, поэтому нам не нужно вручную разлагать их на i32 и эмулировать все инструкции.

Это хорошо и все такое, но в Cheerp нам все еще нужно иметь возможность генерировать код JavaScript для взаимодействия с API-интерфейсами браузера и внешними библиотеками JavaScript.

Более того, поскольку мы никогда не генерировали i64 значений из clang, мы столкнулись с непростой задачей «восстановления» значений и инструкций i64 из их пониженной версии i32. Или, в качестве альтернативы, еще больше измените Clang, чтобы иногда компилировать in64_t в пару i32, а иногда в i64.

Вдобавок ко всему этому нам нужно было решение, которое позволило бы нам в будущем воспользоваться новой функцией JavaScript: BigInt.

В JavaScript теперь есть настоящие целые числа!

BigInt - это новый числовой тип, недавно добавленный в JavaScript. Это позволяет точно представлять и вычислять целые числа произвольного размера. Новый BigInt64Array также позволяет эффективно использовать 64-битные значения в массивах.

Мы можем использовать эти функции для решения проблемы представления таких типов, как int64_t, для кода, скомпилированного в JavaScript.

Единственная проблема заключается в том, что еще не все современные браузеры поддерживают его: Safari будет поддерживать его с версии 14, а старые браузеры, такие как Internet Explorer, просто никогда не будут его поддерживать.

Автоматическое преобразование между JavaScript BigInt и Wasm i64, которое может потребоваться для взаимодействия, в настоящее время поддерживается еще меньше (см. Текущее состояние здесь).

Так что, хотя это очень полезная функция, и Cheerp определенно будет поддерживать использование BigInt при желании, это не окончательное решение проблемы.

Снижение i64 в проходе LLVM

Вместо того, чтобы изменять Clang для компиляции int64_t значений и инструкций непосредственно в i32, мы можем оставить обычную генерацию кода как есть, а позже запустить специальный проход LLVM, чтобы удалить все i64 и преобразовать их в i32.

Это дает то преимущество, что упрощает выполнение прохода по условию и упрощает настраиваемую логику в Clang. Мы все еще можем запустить этот проход в самом начале процесса оптимизации, чтобы получить более эффективный код.

Остается еще одна проблема: совместимость. Например, мы хотим иметь доступ к struct, скомпилированному в Wasm, из функции, скомпилированной в JavaScript.

Наше решение этой проблемы состоит в том, чтобы всегда представлять int64_t как [i32 x 2] в памяти: при выполнении загрузки мы загружаем два i32 и используем их для построения значения i64:

%1 = getelementptr inbounds [2 x i32], [2 x i32]* %0, i32 0, i32 1
%2 = load i32, i32* %1, align 4
%3 = getelementptr inbounds [2 x i32], [2 x i32]* %0, i32 0, i32 0
%4 = load i32, i32* %3, align 4
%5 = zext i32 %2 to i64
%6 = zext i32 %4 to i64
%7 = shl i64 %5, 32
%i = or i64 %7, %6

При выполнении сохранения мы сначала разделяем i64 на два i32 и сохраняем их последовательно:

%8 = lshr i64 %i, 32
%9 = trunc i64 %8 to i32
%10 = trunc i64 %i to i32
%11 = getelementptr inbounds [2 x i32], [2 x i32]* %i.addr, i32 0, i32 1
%12 = getelementptr inbounds [2 x i32], [2 x i32]* %i.addr, i32 0, i32 0
store i32 %9, i32* %11, align 1
store i32 %10, i32* %12, align 1

Таким образом, мы можем обмениваться данными между кодом JavaScript и Wasm, и оба будут правильно интерпретировать 64-битные целые числа.

Для кода, скомпилированного в Wasm, Cheerp фактически выполняет битовую передачу указателя на данные перед загрузкой / сохранением и просто загружает / сохраняет i64 непосредственно по адресу нижнего значения i32 (WebAssembly является прямым порядком байтов). Таким образом мы сохраняем полную эффективность встроенной поддержки i64.

Пример LLVM IR загрузки (для кода, скомпилированного в Wasm):

i.addr = alloca [2 x i32], align 8
%1 = bitcast [2 x i32]* %0 to i64*
%i = load i64, i64* %1, align 4

Пример LLVM IR магазина (для кода, скомпилированного в Wasm):

%2 = bitcast [2 x i32]* %i.addr to i64*
store i64 %i, i64* %2, align 8

Заключение и дальнейшее развитие

Этот новый подход позволяет нам использовать преимущества встроенной 64-битной арифметики И загрузки / сохранения в Wasm, сохраняя при этом достаточно гибкую генерацию кода, чтобы поддерживать простой JavaScript и устаревшую цель asm.js. Все эти улучшения теперь объединены в master и доступны для пользователей ночного PPA. Следующий шаг для нас - необязательно включить использование BigInts, о чем я и расскажу в следующем посте.

У вас есть какие-либо вопросы об этой функции или о чем-либо, связанном с Cheerp? Задайте мне вопрос в Твиттере.