Array digitati - Dati binari nel browser

Ilmari Heikkinen

Introduzione

Gli array di tipo sono una funzionalità relativamente recente dei browser, nata dalla necessità di avere un modo efficiente per gestire i dati binari in WebGL. Un array di tipo è un blocco di memoria con una visualizzazione di tipo, in modo simile al funzionamento degli array in C. Poiché un array di tipo è basato su memoria non elaborata, il motore JavaScript può passare la memoria direttamente alle librerie native senza dover convertire meticolosamente i dati in una rappresentazione nativa. Di conseguenza, gli array con tipi hanno un rendimento molto migliore rispetto agli array JavaScript per il passaggio di dati a WebGL e ad altre API che si occupano di dati binari.

Le visualizzazioni di array digitate fungono da array di tipo singolo per un segmento di un Arraybu. Esistono visualizzazioni per tutti i tipi numerici consueti, con nomi descrittivi come Float32Array, Float64Array, Int32Array e Uint8Array. Esiste anche una visualizzazione speciale che ha sostituito il tipo di array di pixel in ImageData di Canvas: Uint8ClampedArray.

DataView è il secondo tipo di visualizzazione ed è progettato per gestire dati eterogenei. Anziché avere un'API simile a un array, l'oggetto DataView fornisce un'API get/set per leggere e scrivere tipi di dati arbitrari a offset di byte arbitrari. DataView è ottimo per leggere e scrivere intestazioni di file e altri dati simili a struct.

Nozioni di base sull'utilizzo degli array con tipi

Viste di array con tipi

Per utilizzare gli array di tipo, devi creare un ArrayBuffer e una vista. Il modo più semplice è creare una vista array di tipo delle dimensioni e del tipo desiderati.

// Typed array views work pretty much like normal arrays.
var f64a = new Float64Array(8);
f64a[0] = 10;
f64a[1] = 20;
f64a[2] = f64a[0] + f64a[1];

Esistono diversi tipi di visualizzazioni di array con tipi. Condividono tutti la stessa API, quindi se sai come usarne una, saprai quasi come usarle tutte. Nell'esempio seguente creerò una di ciascuna delle viste di array con tipi attualmente esistenti.

// Floating point arrays.
var f64 = new Float64Array(8);
var f32 = new Float32Array(16);

// Signed integer arrays.
var i32 = new Int32Array(16);
var i16 = new Int16Array(32);
var i8 = new Int8Array(64);

// Unsigned integer arrays.
var u32 = new Uint32Array(16);
var u16 = new Uint16Array(32);
var u8 = new Uint8Array(64);
var pixels = new Uint8ClampedArray(64);

L'ultimo è un po' speciale, poiché limita i valori di input tra 0 e 255. Questa funzionalità è particolarmente utile per gli algoritmi di elaborazione delle immagini di Canvas, poiché ora non è necessario limitare manualmente i calcoli di elaborazione delle immagini per evitare il sovraccarico dell'intervallo di 8 bit.

Ad esempio, ecco come applicare un fattore gamma a un'immagine memorizzata in un Uint8Array. Non molto bello:

u8[i] = Math.min(255, Math.max(0, u8[i] * gamma));

Con Uint8ClampedArray puoi saltare il clipping manuale:

pixels[i] *= gamma;

L'altro modo per creare viste di array digitati è prima di tutto creare un Arraybu, quindi creare le viste che puntano a quest'ultimo. Le API che recuperano i dati esterni di solito gestiscono gli ArrayBuffer, quindi in questo modo puoi ottenere una vista di array tipizzato.

var ab = new ArrayBuffer(256); // 256-byte ArrayBuffer.
var faFull = new Uint8Array(ab);
var faFirstHalf = new Uint8Array(ab, 0, 128);
var faThirdQuarter = new Uint8Array(ab, 128, 64);
var faRest = new Uint8Array(ab, 192);

Si possono anche avere più visualizzazioni dello stesso Arraybu.

var fa = new Float32Array(64);
var ba = new Uint8Array(fa.buffer, 0, Float32Array.BYTES_PER_ELEMENT); // First float of fa.

Per copiare un array di tipo in un altro array di tipo, il modo più rapido è utilizzare il metodo di impostazione dell'array di tipo. Per un utilizzo simile a memcpy, crea Uint8Arrays per i buffer delle visualizzazioni e utilizza set per copiare i dati.

function memcpy(dst, dstOffset, src, srcOffset, length) {
  var dstU8 = new Uint8Array(dst, dstOffset, length);
  var srcU8 = new Uint8Array(src, srcOffset, length);
  dstU8.set(srcU8);
};

DataView

Per utilizzare ArrayBuffer che contengono dati con tipi eterogenei, il modo più semplice è utilizzare un DataView per il buffer. Supponiamo di avere un formato file che ha un’intestazione con un int non firmato a 8 bit seguito da due int a 16 bit, seguito da un array di payload di numeri in virgola mobile a 32 bit. Leggere di nuovo questo codice con le viste di array tipizzati è fattibile, ma un po' complicato. Con una DataView possiamo leggere l'intestazione e utilizzare una vista array di tipo per l'array di float.

var dv = new DataView(buffer);
var vector_length = dv.getUint8(0);
var width = dv.getUint16(1); // 0+uint8 = 1 bytes offset
var height = dv.getUint16(3); // 0+uint8+uint16 = 3 bytes offset
var vectors = new Float32Array(width*height*vector_length);
for (var i=0, off=5; i<vectors.length; i++, off+=4) {
  vectors[i] = dv.getFloat32(off);
}

Nell'esempio riportato sopra, tutti i valori che leggo sono in big endian. Se i valori nel buffer sono little-endian, puoi passare il parametro facoltativo littleEndian al getter:

...
var width = dv.getUint16(1, true);
var height = dv.getUint16(3, true);
...
vectors[i] = dv.getFloat32(off, true);
...

Tieni presente che le visualizzazioni di array con tipi sono sempre nell'ordine di byte nativo. Questo serve a velocizzarli. Devi utilizzare una DataView per leggere e scrivere dati in cui l'endianness rappresenta un problema.

DataView dispone anche di metodi per scrivere valori nei buffer. Questi setter sono denominati nello stesso modo dei getter, "set" seguito dal tipo di dati.

dv.setInt32(0, 25, false); // set big-endian int32 at byte offset 0 to 25
dv.setInt32(4, 25); // set big-endian int32 at byte offset 4 to 25
dv.setFloat32(8, 2.5, true); // set little-endian float32 at byte offset 8 to 2.5

Una discussione sull'endianness

L'endianness, o ordine dei byte, è l'ordine in cui i numeri multibyte vengono memorizzati nella memoria del computer. Il termine big-endian descrive un'architettura della CPU che memorizza per prima cosa il byte più significativo; little-endian, il byte meno significativo. L'endianness utilizzato in una determinata architettura della CPU è completamente arbitrario; esistono validi motivi per scegliere uno dei due. Infatti, alcune CPU possono essere configurate per supportare sia i dati big-endian che little-endian.

Perché devi preoccuparti dell'endianness? Il motivo è semplice. Quando leggi o scrivi dati dal disco o dalla rete, devi specificare l'endianness dei dati. In questo modo, i dati vengono interpretati correttamente, indipendentemente dall'endianness della CPU che li utilizza. Nel nostro mondo sempre più in rete, è essenziale supportare correttamente tutti i tipi di dispositivi, big-endian o little-endian, che potrebbero dover lavorare con dati binari provenienti da server o altri peer sulla rete.

L'interfaccia DataView è progettata appositamente per leggere e scrivere dati da e verso i file e la rete. DataView opera sui dati con un endianness specificato. L'endianness, grande o piccola, deve essere specificata a ogni accesso di ogni valore, garantendo di ottenere risultati coerenti e corretti durante la lettura o la scrittura di dati binari, indipendentemente dall'endianness della CPU su cui è in esecuzione il browser.

In genere, quando l'applicazione legge dati binari da un server, devi eseguirne la scansione una volta per convertirli nelle strutture di dati utilizzate internamente dall'applicazione. DataView deve essere utilizzato durante questa fase. Non è consigliabile utilizzare le visualizzazioni di array di tipi multibyte (Int16Array, Uint16Array e così via) direttamente con i dati recuperati tramite XMLHttpRequest, FileReader o qualsiasi altra API di input/output, perché le visualizzazioni di array di tipi utilizzano l'endianness nativo della CPU. Ne parleremo più avanti.

Vediamo un paio di semplici esempi. Il formato file Windows BMP era il formato standard per l'archiviazione delle immagini nei primi tempi di Windows. La documentazione a cui si fa riferimento sopra indica chiaramente che tutti i valori interi nel file sono memorizzati in formato little-endian. Ecco uno snippet di codice che analizza l'inizio dell'intestazione BMP utilizzando la libreria DataStream.js che accompagna questo articolo:

function parseBMP(arrayBuffer) {
  var stream = new DataStream(arrayBuffer, 0,
    DataStream.LITTLE_ENDIAN);
  var header = stream.readUint8Array(2);
  var fileSize = stream.readUint32();
  // Skip the next two 16-bit integers
  stream.readUint16();
  stream.readUint16();
  var pixelOffset = stream.readUint32();
  // Now parse the DIB header
  var dibHeaderSize = stream.readUint32();
  var imageWidth = stream.readInt32();
  var imageHeight = stream.readInt32();
  // ...
}

Ecco un altro esempio, questo tratto dalla demo di rendering High Dynamic Range nel progetto di esempio WebGL. Questa demo scarica dati in virgola mobile poco-endian non elaborati che rappresentano texture ad alta gamma dinamica e deve essere caricato su WebGL. Questo è lo snippet di codice che interpreta correttamente i valori in virgola mobile su tutte le architetture della CPU. Supponiamo che la variabile "arrayBuffer" sia un ArrayBuffer appena scaricato dal server tramite XMLHttpRequest:

var arrayBuffer = ...;
var data = new DataView(arrayBuffer);
var tempArray = new Float32Array(
  data.byteLength / Float32Array.BYTES_PER_ELEMENT);
var len = tempArray.length;
// Incoming data is raw floating point values
// with little-endian byte ordering.
for (var jj = 0; jj < len; ++jj) {
  tempArray[jj] =
    data.getFloat32(jj * Float32Array.BYTES_PER_ELEMENT, true);
}
gl.texImage2D(...other arguments...,
  gl.RGB, gl.FLOAT, tempArray);

La regola generale è: dopo aver ricevuto i dati binari dal server web, esegui un passaggio con una DataView. Leggi i singoli valori numerici e memorizzali in un'altra struttura di dati, ad esempio un oggetto JavaScript (per piccole quantità di dati strutturati) o una visualizzazione di array con tipi (per grandi blocchi di dati). Ciò garantisce che il codice funzioni correttamente su tutti i tipi di CPU. Utilizza anche DataView per scrivere dati in un file o nella rete e assicurati di specificare correttamente l'argomento littleEndian ai vari metodi set per produrre il formato del file che stai creando o utilizzando.

Ricorda che tutti i dati che passano attraverso la rete implicitamente hanno un formato e un'endianness (almeno per qualsiasi valore multi-byte). Assicurati di definire e documentare chiaramente il formato di tutti i dati che l'applicazione invia sulla rete.

API del browser che utilizzano array digitati

Ti fornirò una breve panoramica delle diverse API browser che attualmente utilizzano gli array con tipi. La selezione attuale include WebGL, Canvas, API Web Audio, XMLHttpRequest, WebSocket, Web Worker, API Media Source e API File. Dall'elenco delle API puoi vedere che gli array digitati sono particolarmente adatti per il lavoro multimediale sensibile alle prestazioni e per il trasferimento dei dati in modo efficiente.

WebGL

Il primo utilizzo di array di tipo è stato in WebGL, dove viene utilizzato per trasmettere dati di buffer e dati di immagini. Per impostare i contenuti di un oggetto buffer WebGL, utilizza la chiamata gl.bufferData() con un array di tipi.

var floatArray = new Float32Array([1,2,3,4,5,6,7,8]);
gl.bufferData(gl.ARRAY_BUFFER, floatArray);

Gli array con tipi vengono utilizzati anche per trasmettere i dati delle texture. Ecco un esempio di base di passaggio dei contenuti della trama utilizzando un array di tipo.

var pixels = new Uint8Array(16*16*4); // 16x16 RGBA image
gl.texImage2D(
  gl.TEXTURE_2D, // target
  0, // mip level
  gl.RGBA, // internal format
  16, 16, // width and height
  0, // border
  gl.RGBA, //format
  gl.UNSIGNED_BYTE, // type
  pixels // texture data
);

Inoltre, sono necessari gli array di tipo per leggere i pixel dal contesto WebGL.

var pixels = new Uint8Array(320*240*4); // 320x240 RGBA image
gl.readPixels(0, 0, 320, 240, gl.RGBA, gl.UNSIGNED_BYTE, pixels);

Canvas 2D

Di recente, l'oggetto Canvas ImageData è stato reso compatibile con la specifica degli array di tipi. Ora puoi ottenere una rappresentazione di array di tipi dei pixel in un elemento canvas. Questo è utile perché ora puoi anche creare e modificare array di pixel di canvas senza dover modificare l'elemento canvas.

var imageData = ctx.getImageData(0,0, 200, 100);
var typedArray = imageData.data // data is a Uint8ClampedArray

XMLHttpRequest2

XMLHttpRequest ha ricevuto un miglioramento per gli array di tipo, quindi ora puoi ricevere una risposta di questo tipo anziché dover analizzare una stringa JavaScript in un array di tipo. È molto utile per passare i dati recuperati direttamente alle API multimediali e per analizzare i file binari recuperati dalla rete.

Devi solo impostare responseType dell'oggetto XMLHttpRequest su "arraybuffer".

xhr.responseType = 'arraybuffer';

Ricorda che devi essere consapevole dei problemi di endianness durante il download dei dati dalla rete. Consulta la sezione sull'endianness sopra.

API di file

FileReader può leggere i contenuti del file come ArrayBuffer. Puoi quindi collegare al buffer visualizzazioni di array tipizzati e visualizzazioni dati per manipolarne i contenuti.

reader.readAsArrayBuffer(file);

Anche in questo caso devi tenere presente l'endianness. Per maggiori dettagli, consulta la sezione Endianness.

Oggetti trasferibili

Gli oggetti trasferibili in postMessage consentono di trasmettere i dati binari ad altre finestre e a Web Worker molto più velocemente. Quando invii un oggetto a un worker come trasferibile, l'oggetto diventa inaccessibile nel thread di invio e il worker di destinazione ne acquisisce la proprietà. Ciò consente un'implementazione altamente ottimizzata in cui i dati inviati non vengono copiati, ma solo la proprietà dell'array digitato viene trasferita al destinatario.

Per utilizzare gli oggetti trasferibili con i web worker, devi utilizzare il metodo webkitPostMessage sul worker. Il metodo webkitPostMessage funziona esattamente come postMessage, ma accetta due argomenti anziché uno solo. Il secondo argomento aggiunto è un array di oggetti che vuoi trasferire al worker.

worker.webkitPostMessage(oneGBTypedArray, [oneGBTypedArray]);

Per recuperare gli oggetti dal worker, il worker può trasmetterli di nuovo al thread principale nello stesso modo.

webkitPostMessage({results: grand, youCanHaveThisBack: oneGBTypedArray}, [oneGBTypedArray]);

Nessuna copia, fantastico.

API Media Source

Di recente, gli elementi multimediali hanno anche ottenuto un certo grado di efficacia degli array Typed Array sotto forma di API Media Source. Puoi passare direttamente un array di tipo contenente dati video a un elemento video utilizzando webkitSourceAppend. In questo modo, l'elemento video aggiunge i dati video dopo il video esistente. SourceAppend è ideale per realizzare interstitial, playlist, streaming e per altri utilizzi in cui potresti voler riprodurre più video utilizzando un singolo elemento video.

video.webkitSourceAppend(uint8Array);

WebSocket binari

Puoi anche utilizzare gli array di tipo con WebSocket per evitare di dover concatenare tutti i dati. Ideale per scrivere protocolli efficienti e ridurre al minimo il traffico di rete.

socket.binaryType = 'arraybuffer';

Fiu! Questo conclude la revisione dell'API. Passiamo ora alle librerie di terze parti per la gestione degli array con tipi.

Librerie di terze parti

jDataView

jDataView implementa uno shim DataView per tutti i browser. DataView era una funzionalità solo per WebKit, ma ora è supportata dalla maggior parte degli altri browser. Il team di sviluppatori di Mozilla sta per rilasciare una patch per abilitare DataView anche su Firefox.

Eric Bidelman del team di relazioni con gli sviluppatori di Chrome ha scritto un piccolo esempio di lettore di tag ID3 MP3 che utilizza jDataView. Ecco un esempio di utilizzo tratto dal post del blog:

var dv = new jDataView(arraybuffer);

// "TAG" starts at byte -128 from EOF.
// See http://en.wikipedia.org/wiki/ID3
if (dv.getString(3, dv.byteLength - 128) == 'TAG') {
  var title = dv.getString(30, dv.tell());
  var artist = dv.getString(30, dv.tell());
  var album = dv.getString(30, dv.tell());
  var year = dv.getString(4, dv.tell());
} else {
  // no ID3v1 data found.
}

codifica a stringa

Lavorare con le stringhe negli array digitati è un po' complicato al momento, ma c'è la libreria di codifica delle stringhe che può essere d'aiuto. Stringencoding implementa la specifica di codifica delle stringhe di array di tipo proposta, quindi è anche un buon modo per farsi un'idea di cosa ci aspetta.

Ecco un esempio di utilizzo di base della codifica delle stringhe:

var uint8array = new TextEncoder(encoding).encode(string);
var string = new TextDecoder(encoding).decode(uint8array);

BitView.js

Ho scritto una piccola libreria di manipolazione di bit per gli array di tipo denominata BitView.js. Come suggerisce il nome, funziona in modo molto simile a DataView, tranne per il fatto che funziona con i bit. Con BitView puoi ottenere e impostare il valore di un bit a un determinato offset di bit in un Arraybu. BitView dispone anche di metodi per memorizzare e caricare interi a 6 e 12 bit con offset di bit arbitrari.

Gli int a 12 bit sono ideali per lavorare con le coordinate dello schermo, poiché i display tendono ad avere meno di 4096 pixel lungo la dimensione più lunga. Se utilizzi interi a 12 bit anziché a 32 bit, puoi ottenere una riduzione delle dimensioni del 62%. Per un esempio più estremo, stavo lavorando con shapefile che utilizzano numeri in virgola mobile a 64 bit per le coordinate, ma non avevo bisogno di questa precisione perché il modello doveva essere mostrato solo nelle dimensioni dello schermo. Il passaggio a coordinate base a 12 bit con delta a 6 bit per codificare le modifiche rispetto alla coordinata precedente ha ridotto la dimensione del file a un decimo. Puoi guardare la demo qui.

Ecco un esempio di utilizzo di BitView.js:

var bv = new BitView(arrayBuffer);
bv.setBit(4, 1); // Set fourth bit of arrayBuffer to 1.
bv.getBit(17); // Get 17th bit of arrayBuffer.

bv.getBit(50*8 + 3); // Get third bit of 50th byte in arrayBuffer.

bv.setInt6(3, 18); // Write 18 as a 6-bit int to bit position 3 in arrayBuffer.
bv.getInt12(9); // Read a 12-bit int from bit position 9 in arrayBuffer.

DataStream.js

Uno degli aspetti più interessanti degli array con tipi è il modo in cui semplificano la gestione dei file binari in JavaScript. Anziché analizzare una stringa carattere per carattere e convertire manualmente i caratteri in numeri binari e così via, ora puoi ottenere un ArrayBuffer con XMLHttpRequest ed elaborarlo direttamente utilizzando un DataView. In questo modo, ad esempio, è facile caricare un file MP3 e leggere i tag dei metadati da utilizzare nel tuo audio player. In alternativa, carica uno shapefile e trasformalo in un modello WebGL. Oppure leggi i tag EXIF da un file JPEG e mostrali nell'app per presentazioni.

Il problema degli XHR di ArrayBuffer è che la lettura dei dati di tipo struct dal buffer è un po' scomoda. DataView è utile per leggere alcuni numeri alla volta in modo sicuro per quanto riguarda l'endian, mentre le visualizzazioni di array con tipi sono utili per leggere array di numeri endian nativi allineati alle dimensioni degli elementi. Ci è sembrato mancante un modo per leggere array e strutture di dati in modo pratico e indipendente dall'endian. Inserisci DataStream.js.

DataStream.js è una libreria di array di tipo che legge e scrive scalari, stringhe, array e strutture di dati da ArrayBuffer in modo simile a un file.

Esempio di lettura in un array di numeri in virgola mobile da un ArrayBuffer:

// without DataStream.js
var dv = new DataView(buffer);
var f32 = new Float32Array(buffer.byteLength / 4);
var littleEndian = true;
for (var i = 0; i<f32.length; i++) {
  f32[i] = dv.getFloat32(i*4, littleEndian);
}

// with DataStream.js
var ds = new DataStream(buffer);
ds.endianness = DataStream.LITTLE_ENDIAN;
var f32 = ds.readFloat32Array(ds.byteLength / 4);

DataStream.js diventa davvero utile per la lettura di dati più complessi. Supponiamo di avere un metodo che legga gli indicatori JPEG:

// without DataStream.js
var dv = new DataView(buffer);
var objs = [];
for (var i=0; i<buffer.byteLength;) {
  var obj = {};
  obj.tag = dv.getUint16(i);
  i += 2;
  obj.length = dv.getUint16(i);
  i += 2;
  obj.data = new Uint8Array(obj.length - 2);
  for (var j=0; j<obj.data.length; j++,i++) {
    obj.data[j] = dv.getUint8(i);
  }
  objs.push(obj);
}

// with DataStream.js
var ds = new DataStream(buffer);
ds.endianness = ds.BIG_ENDIAN;
var objs = [];
while (!ds.isEof()) {
  var obj = {};
  obj.tag = ds.readUint16();
  obj.length = ds.readUint16();
  obj.data = ds.readUint8Array(obj.length - 2);
  objs.push(obj);
}

In alternativa, utilizza il metodo DataStream.readStruct per leggere strutture di dati. Il metodo readStruct riceve un array di definizioni di struct che contiene i tipi dei membri dello struct. Dispone di funzioni di callback per la gestione di tipi complessi e gestisce anche array di dati e strutture nidificate:

// with DataStream.readStruct
ds.readStruct([
  'objs', ['[]', [ // objs: array of tag,length,data structs
    'tag', 'uint16',
    'length', 'uint16',
    'data', ['[]', 'uint8', function(s,ds){ return s.length - 2; }], // get length with a function
  '*'] // read in as many struct as there are
]);

Come puoi vedere, la definizione dello struct è un array piatto di coppie [name, type]. Le strutture nidificate vengono create disponendo di un array per il tipo. Gli array vengono definiti utilizzando un array di tre elementi, in cui il secondo elemento è il tipo di elemento dell'array e il terzo è la lunghezza dell'array (come numero, come riferimento al campo letto in precedenza o come funzione di callback). Il primo elemento della definizione dell'array non è utilizzato.

I valori possibili per il tipo sono i seguenti:

Number types

Unsuffixed number types use DataStream endianness.
To explicitly specify endianness, suffix the type with
'le' for little-endian or 'be' for big-endian,
e.g. 'int32be' for big-endian int32.

  'uint8' -- 8-bit unsigned int
  'uint16' -- 16-bit unsigned int
  'uint32' -- 32-bit unsigned int
  'int8' -- 8-bit int
  'int16' -- 16-bit int
  'int32' -- 32-bit int
  'float32' -- 32-bit float
  'float64' -- 64-bit float

String types

  'cstring' -- ASCII string terminated by a zero byte.
  'string:N' -- ASCII string of length N.
  'string,CHARSET:N' -- String of byteLength N encoded with given CHARSET.
  'u16string:N' -- UCS-2 string of length N in DataStream endianness.
  'u16stringle:N' -- UCS-2 string of length N in little-endian.
  'u16stringbe:N' -- UCS-2 string of length N in big-endian.

Complex types

  [name, type, name_2, type_2, ..., name_N, type_N] -- Struct

  function(dataStream, struct) {} -- Callback function to read and return data.

  {get: function(dataStream, struct) {}, set: function(dataStream, struct) {}}
  -- Getter/setter functions to reading and writing data. Handy for using the
     same struct definition for both reading and writing.

  ['', type, length] -- Array of given type and length. The length can be either
                        a number, a string that references a previously-read
                        field, or a callback function(struct, dataStream, type){}.
                        If length is set to '*', elements are read from the
                        DataStream until a read fails.

Qui puoi vedere un esempio pratico di lettura dei metadati JPEG. La demo utilizza DataStream.js per leggere la struttura a livello di tag del file JPEG (insieme ad alcune analisi EXIF) e jpg.js per decodificare e visualizzare l'immagine JPEG in JavaScript.

Storia degli array con tipi

Gli array di tipo sono stati introdotti nella fase iniziale di implementazione di WebGL, quando abbiamo scoperto che il passaggio di array JavaScript al driver grafico causava problemi di prestazioni. Con gli array JavaScript, l'associazione WebGL doveva allocare un array nativo e riempirlo esaminando l'array JavaScript e trasmettendo ogni oggetto JavaScript nell'array al tipo nativo richiesto.

Per risolvere il problema del collo di bottiglia della conversione dei dati, Vladimir Vukicevic di Mozilla ha scritto CanvasFloatArray: un array di float in stile C con un'interfaccia JavaScript. Ora puoi modificare CanvasFloatArray in JavaScript e passarlo direttamente a WebGL senza dover fare altro nella definizione. In ulteriori iterazioni, CanvasFloatArray è stato rinominato WebGLFloatArray, che è stato ulteriormente rinominato in Float32Array e suddiviso in un Arraybu di supporto e in Float32Array-view per accedere al buffer. Sono stati aggiunti anche tipi per altri numeri interi e in virgola mobile e per le varianti con segno/senza segno.

Note sul layout

Fin dall'inizio, la progettazione degli array con tipi è stata guidata dalla necessità di trasmettere in modo efficiente i dati binari alle librerie native. Per questo motivo, le viste dell'array tipizzato operano su dati allineati nell'endianità nativa della CPU host. Queste decisioni consentono a JavaScript di raggiungere le massime prestazioni durante operazioni come l'invio di dati dei vertici alla scheda grafica.

DataView è progettato specificamente per l'I/O di file e rete, in cui i dati hanno sempre un endianness specificato e potrebbero non essere allineati per le massime prestazioni.

La suddivisione del design tra l'assemblaggio dei dati in memoria (utilizzando le visualizzazioni di array con tipi) e l'I/O (utilizzando DataView) è stata consapevole. I motori JavaScript moderni ottimizzano notevolmente le visualizzazioni degli array con tipi e raggiungono prestazioni elevate con le operazioni numeriche. Questa decisione di progettazione ha reso possibili gli attuali livelli di prestazioni delle visualizzazioni degli array digitati.

Riferimenti