Design: Самый эффективный способ передачи контекста данных JS / WASM

Созданный на 12 сент. 2018  ·  18Комментарии  ·  Источник: WebAssembly/design

Привет,

давайте представим, что у меня есть несколько 1024 f32 значений, хранящихся в некотором буфере Float32Array , ожидающих обработки кодом DSP на основе WASM :)

Я все еще новичок в WebAssembly. Я понял, что в качестве аргументов экспортируемым функциям WASM можно передавать только типизированные числовые значения. Для меня это имеет смысл, и поэтому я решил передавать свои данные, используя память. Меня это тоже устраивает ...

Итак, чтобы передать мои 1024 значения, я напрямую присваиваю их .memory . Нравиться:

const mem = exports.memory.buffer;
const F32 = new Float32Array(mem);
F32[0] = 31337.777;

Это весело, но чтобы присвоить все свои значения, мне нужно перебрать все значения, чтобы назначить их все в памяти. Что ж, почему-то это кажется неправильным с точки зрения производительности. Я ожидал родной имп. сделать это для меня. например, ключ , как data в memoryDescriptor аргументе WebAssembly.Memory конструктора , позволяющего инициализировать память с ArrayBuffer .

Итак, что ж, тогда я выполняю вызов функции WASM, и когда WASM impl. сделал это волшебство, он записывает значения результата обратно в память. Это происходит внутри цикла DSP, поэтому в моем коде WASM для этого нет накладных расходов - насколько я могу судить.

Но теперь, когда я вернулся в контекст JS, мне нужно снова перебрать всю память, чтобы прочитать все значения и построить другое представление данных на основе JS. И как-то ожидал родной имп. присутствовать и для этой цели. Может быть, что-то вроде memory.read(Float32Array) чтобы вернуть мне данные буфера в виде Float32Array , абстрагируя указатель и итерационный лабиринт.

Я что-то упускаю?
Есть ли лучший способ передать большие объемы данных из / в WASM, который я просто упустил из виду?

Заранее спасибо и всего наилучшего,
Арон

Самый полезный комментарий

Вы должны решить, хотите ли вы копировать данные между JavaScript и WebAssembly или хотите, чтобы WebAssembly владела данными.

Если вы хотите скопировать данные, вы можете использовать TypedArray.prototype.set () вместо того, чтобы писать цикл for самостоятельно:

let instance = ...;
let myJSArray = new Float32Array(...);
let length = myJSArray.length;
let myWasmArrayPtr = instance.exports.allocateF32Array(length);
let myWasmArray = new Float32Array(instance.exports.memory.buffer, myWasmArrayPtr, length);

// Copy data in to be used by WebAssembly.
myWasmArray.set(myJSArray);

// Process the data in the array.
instance.exports.processF32Array(myWasmArrayPtr, length);

// Copy data out to JavaScript.
myJSArray.set(myWasmArray);

Если WebAssembly владеет данными, вы можете создать представление для буфера WebAssembly.Memory и передать его также своим функциям JavaScript:

let instance = ...;
let length = ...;
let myArrayPtr = instance.exports.allocateF32Array(length);
let myArray = new Float32Array(instance.exports.memory.buffer, myArrayPtr, length);

// Use myArray as a normal Float32Array in JavaScript, fill in data, etc.
...

// Process the data in the array.
instance.exports.processF32Array(myArrayPtr, length);

// No need to copy data back to JavaScript, just use myArray directly.

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

Все 18 Комментарий

Вы должны решить, хотите ли вы копировать данные между JavaScript и WebAssembly или хотите, чтобы WebAssembly владела данными.

Если вы хотите скопировать данные, вы можете использовать TypedArray.prototype.set () вместо того, чтобы писать цикл for самостоятельно:

let instance = ...;
let myJSArray = new Float32Array(...);
let length = myJSArray.length;
let myWasmArrayPtr = instance.exports.allocateF32Array(length);
let myWasmArray = new Float32Array(instance.exports.memory.buffer, myWasmArrayPtr, length);

// Copy data in to be used by WebAssembly.
myWasmArray.set(myJSArray);

// Process the data in the array.
instance.exports.processF32Array(myWasmArrayPtr, length);

// Copy data out to JavaScript.
myJSArray.set(myWasmArray);

Если WebAssembly владеет данными, вы можете создать представление для буфера WebAssembly.Memory и передать его также своим функциям JavaScript:

let instance = ...;
let length = ...;
let myArrayPtr = instance.exports.allocateF32Array(length);
let myArray = new Float32Array(instance.exports.memory.buffer, myArrayPtr, length);

// Use myArray as a normal Float32Array in JavaScript, fill in data, etc.
...

// Process the data in the array.
instance.exports.processF32Array(myArrayPtr, length);

// No need to copy data back to JavaScript, just use myArray directly.

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

Вау, спасибо, @binji, это выглядит потрясающе. Это именно то, что я искал. Также спасибо, что нашли время написать пример кода. Это тоже очень полезно. Я попробую сегодня вечером и сделаю запрос на внесение некоторых улучшений в загрузчик. который используется для взаимодействия с WASM в https://github.com/AssemblyScript/assemblyscript :) (Вот почему я не наткнулся на embind, который отлично выглядит для использования с emscripten)

Видите ли вы возможность расширить документацию на webassembly.org по этому поводу?

Например здесь:

https://webassembly.org/docs/web/

Я готов сделать это, если возможно.

Думаю, было бы неплохо объяснить это здесь:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Memory

@binji Не по теме, вам может понравиться https://github.com/torch2424/wasmBoy, если вы еще не знаете о проекте. И этот проект также может выиграть от предложенного вами решения, поскольку они используют циклы for для установки памяти ROM.

Видите ли вы возможность расширить документацию на webassembly.org по этому поводу?

Решили, что мы должны обновить webassembly.org, внося это изменение и многое другое. Документы все еще из репозитория дизайна, но репозиторий спецификаций намного более свежий и точный.

Добавление некоторых примеров в MDN, вероятно, было бы проще и лучше, учитывая, что гораздо больше веб-разработчиков будут использовать этот сайт, чем webassembly.org.

Не по теме, вам может понравиться https://github.com/torch2424/wasmBoy, если вы еще не знаете о проекте.

Да, я знаю @ torch2424. :-) У меня тоже есть свой эмулятор gameboy на основе wasm: https://github.com/binji/binjgb

Добавление некоторых примеров в MDN, вероятно, было бы проще и лучше, учитывая, что гораздо больше веб-разработчиков будут использовать этот сайт, чем webassembly.org.

Хорошо, я опубликую сегодня новости. Посмотрим, будет ли оно опубликовано.

Да, я знаю @ torch2424. :-) У меня тоже есть свой эмулятор gameboy на основе wasm: https://github.com/binji/binjgb

Ха-ха, круто! Я буду деф. попробуйте :) Держу пари, ваш эмулятор не вылетает в Zelda - Link's Awakening, когда вы получаете меч (на пляже);))

Я надеюсь найти время, чтобы исправить сбой и в wasmBoy :)

Привет! Рад, что ты нашел васмобой! 😄 Да, эмулятор @binji определенно более точен, ха-ха! Над чем обязательно поработать. Но, по иронии судьбы, я все время использую пробуждение Линка, чтобы проверить васмбая, ха-ха! Я удивлен, что он разбился о вас.

На основании отзывов обнаружил несколько ошибок:
https://github.com/torch2424/wasmBoy/issues/141 - Передача памяти
https://github.com/torch2424/wasmBoy/issues/142 - Zelda Crash (со скриншотами у меня работает)

В любом случае, не хочу сорвать разговор / проблему, я перейду к тем, которые открыл. Но спасибо за отзыв! 😄

Если WebAssembly владеет данными, вы можете создать представление над буфером WebAssembly.Memory и передать его своим функциям JavaScript:

Одно важное предостережение: представление в буфер памяти будет недействительным, если экземпляр WebAssembly увеличит свою память. Избегайте сохранения любых ссылок на представление, которые сохраняются после последующих вызовов экземпляра WebAssembly.

@binji Можете ли вы сказать мне, что вы

let myWasmArrayPtr = instance.exports.allocateF32Array(length);

Я пишу следующее, и это работает, но я все еще хочу знать, правильно ли это.

export function allocateF32Array(length: usize): usize {
    return memory.allocate(length * sizeof<f32>());
}

Нужно ли мне писать другую функцию AS, чтобы освободить ее с помощью memory.free() , и вызывать ее после передачи массива на сторону JS?

@fmkang Итак, то, что объяснил @binji, было на стороне JS (пожалуйста, поправьте меня, если я ошибаюсь).

Используя типизированные массивы , вы можете выполнять эффективную запись в память WASM со стороны JS, используя set () с буфером массивов Wasm.

При записи в память Wasm изнутри Wasm / AS я все еще использую стандартный подход для цикла, например: https://github.com/torch2424/wasmBoy/blob/master/core/memory/dma.ts#L29

Хотя, возможно, @MaxGraey или @dcodeIO могли бы помочь с тем, как сделать это наиболее эффективно изнутри AS или Wasm land? 😄 Хотя, возможно, нам лучше перенести это в репо AS

@fmkang Ты бы просто

export function allocateF32Array(length: i32): Float32Array {
  return new Float32Array(length);
}

на стороне AS и

let myArray = module.getArray(Float32Array, module.allocateF32Array(length));

на стороне JS с помощью загрузчика .

@ torch2424 Да, код @binji находится на стороне JS, но let myWasmArrayPtr = instance.exports.allocateF32Array(length) вызывает функцию с именем allocateF32Array в модуле Wasm (вероятно, скомпилированном AssembleScript). Эта функция не упоминается ни в его фрагменте, ни во встроенных функциях AS, поэтому я думаю, что я должен ее реализовать. Я спрашиваю, как получить этот указатель, указывающий на соответствующий массив Wasm.

@dcodeIO Спасибо. Похоже, это упрощает передачу массивов. Я буду тщательно экспериментировать, прежде чем размещать здесь новые комментарии. Но мой модуль вылетает при загрузке после того, как я заменяю свою функцию вашей. Консоль говорит

astest.html:1 Uncaught (in promise) TypeError: WebAssembly Instantiation: Import #0 module="env" error: module is not an object or function

Он даже вылетает, если я напишу что-то вроде let a = new Float32Array(10); в любом месте моего кода AS.

@dcodeIO Модуль Wasm аварийно завершает работу, когда мы пытаемся инициализировать массив с помощью выражений (вместо литералов, таких как let c: f64[] = [a[0] + b[0], a[1] + b[1]]; ) или присваивать значения элементам массива (например, let a: f64[] = [0, 0, 0]; a[0] = 1; или let array = new Array<i32>(); array.push(1); ).

Сначала мы думали, что это потому, что мы не поместили memory в env , поэтому мы написали:

var importObject = {
    env: { memory: new WebAssembly.Memory({initial:10}) },
    imports: { imported_func: arg => console.log(arg) }
};
WebAssembly.instantiate(wasmBinary, importObject).then(...);

Но проблема все еще существовала. Потом мы обнаружили, что это из-за отсутствия abort . Мы написали:

env: {
    abort(msg, file, line, column) {
        console.error("abort called at main.ts:" + line + ":" + column);
    }
}

или просто env: { abort: function(){} } , и проблема решена. Однако сообщение об ошибке отсутствует, и выполнение кода на самом деле не «прерывается». Мы до сих пор не знаем истинную причину этой проблемы.

Мы действительно новички в WebAssembly. Я пишу этот пост только для того, чтобы дать обновленную информацию. Вам действительно не нужно отвечать.

@fmkang Похоже, вы не используете загрузчик AssemblyScript для создания экземпляра модуля. Хотя вы можете реализовать все это самостоятельно, загрузчик уже добавляет базовую функциональность к экспорту модуля, например, функцию прерывания. Если у вас есть еще вопросы по AssemblyScript, не стесняйтесь задавать их в нашем трекере проблем, и мы будем рады помочь :)

Итак, я пробовал использовать эту технику:

let myArrayPtr = instance.exports.allocateF32Array(length);
let myArray = new Float32Array(instance.exports.memory.buffer, myArrayPtr, length);

// Use myArray as a normal Float32Array in JavaScript, fill in data, etc.
...

// Process the data in the array.
instance.exports.processF32Array(myArrayPtr, length);

Но моя функция processF32Array дает RuntimeError: memory access out of bounds . Загрузчиком не пользуюсь, FWIW. Я также пробовал:

let myArray = module.getArray(Float32Array, module.allocateF32Array(length));

Но в моем модуле нет getArray, и я не уверен, как он должен его получить.

Я искал, как копировать массивы в / из WebAssembly, и наткнулся на эту ветку. Для кого-то еще я смог скопировать массивы с помощью AssemblyScript и библиотеки @ torch2424 as-bind

Вот мой простой тест:

AssemblyScript

export function sum(arr: Float64Array): f64 {
  let sum: f64 = 0;
  for(let i = 0; i < arr.length; i++) {
    sum += arr[i];
  }
  return sum;
}

JavaScript (узел)

const { AsBind } = require("as-bind");
const fs = require("fs");
const wasm = fs.readFileSync(__dirname + "/build/optimized.wasm");

const asyncTask = async () => {
  const asb = await AsBind.instantiate(wasm);

  // Make a large array
  console.time('Making array');
  let arr = new Float64Array(1e8).fill(1);
  console.timeEnd('Making array');

  // Find the sum using reduce
  console.time('Reduce');
  let sum1 = arr.reduce((acc, val) => acc + val, 0);
  console.timeEnd('Reduce');

  // Find the sum with for loops
  console.time('JS For');
  let sum2 = 0;
  for(let i = 0; i < arr.length; i++) sum2 += arr[i];
  console.timeEnd('JS For');

  // Find the sum with WebAssembly
  console.time('Wasm For');
  let sum3 = asb.exports.sum(arr);
  console.timeEnd('Wasm For');

  console.log(sum1, sum2, sum3);
};

asyncTask();

На моей машине я получил следующий результат:

Making array: 789.086ms
Reduce: 2452.922ms
JS For: 184.818ms
Wasm For: 2008.482ms
100000000 100000000 100000000

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

Редактировать:

Я удалил суммирование из AssemblyScript, чтобы изолировать время работы только копии массива:

export function sum(arr: Float64Array): f64 {
  let sum: f64 = 0;
  return sum;
}

И я перепроверил тесты.

Без суммирования Wasm

Making array: 599.826ms
Reduce: 2810.395ms
JS For: 188.623ms
Wasm For: 762.481ms
100000000 100000000 0

Похоже, что самой копии данных потребовалось 762,481 мс, чтобы скопировать более 100 миллионов f64 s.

@pwstegman Я создал еще один тест, который использует случайные входные данные и не позволяет движку js оптимизировать ваш цикл: https://webassembly.studio/?f=5ux4ymi345e

И результаты отличаются для Chrome и FF:

Chrome 81.0.4044.129

js sum: 129.968017578125ms
sum result = 49996811.62100115

js reduce: 1436.532958984375ms
js reduce result = 49996811.62100115

wasm sum: 153.000244140625ms
wasm sum result = 49996811.62100115

wasm reduce: 125.009033203125ms
wasm reduce result = 49996811.62100115

wasm empty: 0.002685546875ms

Привет.
Я в основном новичок в отношении знакомства с WASM, и я наткнулся на ту же проблему, что и люди здесь и в # 1162. (Я спрашиваю здесь против # 1162, так как этот имеет более недавнюю активность.)
После краткого исследования я сделал демонстрацию , пытаясь понять это, и в итоге получил метод, в котором вы предоставляете представления в память WASM в виде типизированных массивов, чтобы избежать копирования данных внутрь и наружу, вот суть этого с точки зрения JS :

// need to know the size in advance, that's OK since the impl makes some init 
const fft = new Module.KissFftReal(/*size=*/N);depending on the size anyway

// get view into WASM memory as Float64Array (of size=N in this case)
const input = fft.getInputTimeDataBuffer();

// fill 'input' buffer

// perform transformation, view into WASM memory is returned here (of size=(N + 2) in this case, +2 for Nyquist bin)
const output = fft.transform();

// use transformation result returned in 'output' buffer

Это путь? Есть ли лучшее решение? Буду признателен за любые комментарии / обновления по этой теме.

ps хоть и уродливо, но на практике он отлично работает для меня (насколько я понимаю и ценю)

pps
вот мой (пока без ответа) вопрос о стеке по поводу аналогичной проблемы:

https://stackoverflow.com/questions/65566923/is-there-a-more-efficient-way-to-return-arrays-from-c-to-javascript

Была ли эта страница полезной?
0 / 5 - 0 рейтинги

Смежные вопросы

cretz picture cretz  ·  5Комментарии

Artur-A picture Artur-A  ·  3Комментарии

bobOnGitHub picture bobOnGitHub  ·  6Комментарии

ghost picture ghost  ·  7Комментарии

artem-v-shamsutdinov picture artem-v-shamsutdinov  ·  6Комментарии