Design: Maneira mais eficiente de passar o contexto JS / WASM de dados

Criado em 12 set. 2018  ·  18Comentários  ·  Fonte: WebAssembly/design

Oi,

vamos imaginar que tenho cerca de 1024 f32 valores armazenados em algum buffer Float32Array , esperando para serem processados ​​pelo código DSP baseado em WASM :)

Ainda sou muito novo no WebAssembly. Eu entendi que você pode passar apenas valores numéricos digitados como argumentos para funções WASM exportadas. Isso faz sentido para mim e é por isso que decidi passar meus dados usando a memória. Eu estou bem com isso também ...

Portanto, para passar meus 1024 valores, eu os atribuo a .memory diretamente. Gostar:

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

Isso é divertido, mas para atribuir todos os meus valores, tenho que repetir todos os valores para atribuí-los à memória. Bem, de alguma forma isso parece errado em termos de desempenho. Eu esperava um implante nativo. para fazer isso por mim. por exemplo, uma chave como data no memoryDescriptor argumento do WebAssembly.Memory construtor que permite inicializar a memória com um ArrayBuffer .

Então, bem, eu faço minha chamada de função WASM e quando o WASM impl. é mágico, é escrever os valores do resultado de volta na memória. Isso está acontecendo dentro do loop DSP, portanto, não há sobrecarga dentro do meu código WASM para fazer isso - até onde posso ver.

Mas agora, depois que estou de volta ao contexto JS, tenho que iterar em toda a memória novamente apenas para ler todos os valores e construir outra representação de dados baseada em JS. E de alguma forma eu esperava um implante nativo. para estar presente também para este propósito. Talvez algo como memory.read(Float32Array) para me retornar os dados do buffer como um Float32Array , abstraindo o labirinto de ponteiro e iteração.

Estou esquecendo de algo?
Existe uma maneira melhor de passar grandes quantidades de dados de / para WASM que acabei de ignorar?

Agradecemos antecipadamente e melhor,
Aron

Comentários muito úteis

Você deve decidir se deseja copiar dados entre JavaScript e WebAssembly ou se deseja que WebAssembly seja o proprietário dos dados.

Se quiser copiar os dados, você pode usar TypedArray.prototype.set () em vez de escrever o loop for você mesmo:

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);

Se WebAssembly possuir os dados, você pode criar uma visualização sobre o buffer WebAssembly.Memory e passá-la para suas funções JavaScript também:

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.

Você também pode usar ferramentas como embind para tornar isso um pouco mais fácil de usar.

Todos 18 comentários

Você deve decidir se deseja copiar dados entre JavaScript e WebAssembly ou se deseja que WebAssembly seja o proprietário dos dados.

Se quiser copiar os dados, você pode usar TypedArray.prototype.set () em vez de escrever o loop for você mesmo:

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);

Se WebAssembly possuir os dados, você pode criar uma visualização sobre o buffer WebAssembly.Memory e passá-la para suas funções JavaScript também:

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.

Você também pode usar ferramentas como embind para tornar isso um pouco mais fácil de usar.

Uau, obrigado @binji, isso parece incrível. É exatamente o que eu estava procurando. Obrigado também por escrever o código de exemplo. Isso também é muito útil. Vou experimentar esta noite e solicitar algumas melhorias para o carregador impl. que está em uso para interface com WASM em https://github.com/AssemblyScript/assemblyscript :) (É por isso que não encontrei embind, que parece incrível para uso com emscripten)

Você vê uma opção para estender os documentos em webassembly.org em relação a isso?

Por exemplo aqui:

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

Estou disposto a fazer isso, se possível.

Acho que também seria uma boa ideia explicá-lo aqui:

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

@binji Off-topic, você pode gostar de https://github.com/torch2424/wasmBoy se ainda não conhece o projeto. E este projeto também pode se beneficiar da solução que você propôs, já que eles usam for-loops para configurar a memória ROM.

Você vê uma opção para estender os documentos em webassembly.org em relação a isso?

Concordamos que deveríamos atualizar webassembly.org, com essa mudança e muito mais. Os documentos ainda existem do repositório de design, mas o repositório de especificações é muito mais recente e preciso.

Adicionar alguns exemplos ao MDN provavelmente seria mais fácil e melhor, considerando que muitos mais desenvolvedores da Web usarão esse site do que webassembly.org.

Fora do tópico, você pode gostar de https://github.com/torch2424/wasmBoy se ainda não estiver ciente do projeto.

Sim, eu sei @ torch2424. :-) Eu também tenho meu próprio emulador de gameboy baseado em wasm: https://github.com/binji/binjgb

Adicionar alguns exemplos ao MDN provavelmente seria mais fácil e melhor, considerando que muitos mais desenvolvedores da Web usarão esse site do que webassembly.org.

Tudo bem, vou postar uma atualização esta noite. Vamos ver se isso é publicado.

Sim, eu sei @ torch2424. :-) Eu também tenho meu próprio emulador de gameboy baseado em wasm: https://github.com/binji/binjgb

Haha legal! Eu vou derrotar. experimente :) Aposto que seu emulador não falha em Zelda - Link's Awakening quando você pega a espada (na praia);))

Espero encontrar algum tempo para consertar a falha no wasmBoy também :)

Ei! Rad que você encontrou o wasmboy! 😄 Sim, o emulador de @binji é definitivamente muito mais preciso haha! Algo em que preciso trabalhar, com certeza. Mas, ironicamente, eu uso o despertar de Link para testar wasmboy o tempo todo haha! Estou surpreso que tenha batido em você.

Alguns bugs foram abertos com base no feedback:
https://github.com/torch2424/wasmBoy/issues/141 - Passagem de memória
https://github.com/torch2424/wasmBoy/issues/142 - Zelda Crash (com capturas de tela funciona para mim)

De qualquer forma, não quero atrapalhar a conversa / problema, vou passar para os problemas que abri. Mas obrigado pelo feedback! 😄

Se WebAssembly possuir os dados, você pode criar uma visualização no buffer WebAssembly.Memory e passá-la para suas funções JavaScript também:

Uma advertência importante: a exibição no buffer de memória será invalidada se a instância de WebAssembly aumentar sua memória. Evite armazenar quaisquer referências à visualização que persistem após chamadas posteriores na instância WebAssembly.

@binji Você pode me dizer o que você coloca no lado do AssemblyScript quando usa isso? Percebi que não é uma função padrão.

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

Escrevo da seguinte maneira e funciona, mas ainda quero saber se está correto.

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

Preciso escrever outra função AS para liberá-la com memory.free() e chamá-la depois de transferir a matriz para o lado JS?

@fmkang Então, o que @binji explicou foi no lado JS (por favor, corrija-me se eu estiver errado).

Usando Typed Arrays , você pode fazer uma escrita eficiente na memória WASM do lado JS usando set () com o Wasm Array Buffer.

Escrevendo na memória Wasm de dentro do Wasm / AS, ainda estou usando o padrão para abordagem de loop, por exemplo: https://github.com/torch2424/wasmBoy/blob/master/core/memory/dma.ts#L29

Porém, talvez @MaxGraey ou @dcodeIO possam ajudar a fazer isso de forma mais eficiente de dentro do AS ou Wasm? 😄 Embora seja melhor movermos isso para o repositório AS

@fmkang Você simplesmente

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

no lado AS e

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

no lado JS, usando o carregador .

@ torch2424 Sim, o código de @binji está no lado JS, mas let myWasmArrayPtr = instance.exports.allocateF32Array(length) está chamando uma função chamada allocateF32Array no módulo Wasm (provavelmente compilado por AssembleScript). Esta função não é mencionada em seu snippet nem faz parte das funções integradas do AS, então acho que deve ser implementada por mim mesmo. Estou perguntando como obter este ponteiro, que aponta para a matriz Wasm correspondente.

@dcodeIO Obrigado. Parece que isso simplifica a passagem de matrizes. Vou experimentar com cuidado antes de postar novos comentários aqui. Mas meu módulo trava ao ser carregado depois que substituo minha função pela sua. O console diz

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

Ele até trava se eu escrever algo como let a = new Float32Array(10); em qualquer lugar do meu código AS.

@dcodeIO O módulo Wasm falha quando tentamos inicializar um array com expressões (em vez de literais, como let c: f64[] = [a[0] + b[0], a[1] + b[1]]; ) ou atribuir valores a elementos de um array (como let a: f64[] = [0, 0, 0]; a[0] = 1; ou let array = new Array<i32>(); array.push(1); ).

A princípio, pensamos que isso acontecia porque não colocamos memory em env , então escrevemos:

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

Mas o problema ainda existia. Então descobrimos que era por causa da falta de abort . Nós escrevemos:

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

ou simplesmente env: { abort: function(){} } , e o problema resolvido. No entanto, não há mensagem de erro e a execução do código não foi realmente "abortada". Ainda não sabemos a verdadeira causa deste problema.

Somos realmente novos no WebAssembly. Escrevo este post apenas para dar uma atualização. Você realmente não precisa responder.

@fmkang Parece que você não está usando o carregador do nosso rastreador de problemas e ficaremos felizes em ajudar :)

Então, tentei usar esta técnica:

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);

Mas minha função processF32Array resulta em RuntimeError: memory access out of bounds . Não estou usando o carregador, FWIW. Eu também tentei:

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

Mas meu módulo não tem um "getArray" e não tenho certeza de como ele deve obtê-lo.

Eu estava procurando como copiar matrizes de / para WebAssembly e me deparei com este tópico. Para qualquer outra pessoa, consegui copiar matrizes usando a biblioteca do AssemblyScript e @ torch2424 como-bind

Este é meu teste simples:

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 (Nó)

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();

Em minha máquina, obtive a seguinte saída:

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

Parece que há uma boa sobrecarga ao copiar o array para WebAssembly com esse método, embora eu possa imaginar que valerá a pena por uma operação mais cara do que uma soma.

Editar:

Removi o somatório do AssemblyScript para isolar o tempo de execução apenas da cópia da matriz:

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

E eu repassei os benchmarks.

Sem soma de Wasm

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

Portanto, parece que a própria cópia dos dados levou 762,481 ms para copiar mais de 100 milhões de f64 s.

@pwstegman Eu criei outro benchmark que usa dados de entrada aleatórios e evita que o mecanismo js otimize seu loop: https://webassembly.studio/?f=5ux4ymi345e

E os resultados são diferentes para Chrome e 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

Olá.
Sou basicamente um recém-nascido em relação à familiaridade com WASM e me deparei com o mesmo problema que as pessoas aqui e em # 1162. (Estou perguntando aqui em relação ao nº 1162, já que este tem atividades mais recentes.)
Após uma breve pesquisa, fiz uma demonstração enquanto tentava descobrir e acabei com um método em que você expõe visualizações na memória WASM como matrizes digitadas para evitar a cópia de dados dentro e fora, aqui está a essência do ponto de vista 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

É este o caminho a percorrer? Existe alguma solução melhor? Agradeceria qualquer comentário / atualização sobre este tópico.

PS embora seja meio feio, funciona muito bem para mim na prática (tanto quanto eu posso entender e apreciar)

pps
aqui está minha (ainda sem resposta) questão stackoverflow com relação a uma preocupação semelhante:

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

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

JimmyVV picture JimmyVV  ·  4Comentários

Artur-A picture Artur-A  ·  3Comentários

beriberikix picture beriberikix  ·  7Comentários

bobOnGitHub picture bobOnGitHub  ·  6Comentários

arunetm picture arunetm  ·  7Comentários