Недавно я улучшил поддержку 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-битных инструкций для i32
type. Это разложение было выполнено непосредственно в 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? Задайте мне вопрос в Твиттере.