As matrizes de Javascript são esparsas?

97

Ou seja, se eu usar a hora atual como um índice na matriz:

array[Date.getTime()] = value;

o interpretador instanciará todos os elementos de 0 até agora? Navegadores diferentes fazem isso de maneira diferente?

Lembro que costumava haver um bug no kernel AIX , que criava pseudo-ttys a pedido, mas se você fizesse, digamos, "echo> / dev / pty10000000000", ele criaria / dev / pty0, / dev / pty1, .... e então cair morto. Era divertido em feiras, mas não quero que isso aconteça com meus clientes.

baga
fonte
1
Uma possível desvantagem de fazer isso é a dificuldade de depuração no Firebug. uma instrução de log na matriz listará apenas os primeiros 1000 elementos na matriz, que serão todos "indefinidos". Além disso, array.length dirá que seu array possui n elementos, embora n-1 sejam apenas valores indefinidos "fantasmas".
Michael Butler
A depuração agora está OK no Chrome - aqui está um exemplo de saída do console: [vazio × 9564, Objeto, vazio × 105, Objeto, vazio × 10, Objeto, vazio × 12, Objeto, vazio × 9, Objeto, vazio × 21, Object, empty × 9, Object]
jsalvata

Respostas:

40

Como exatamente as matrizes de JavaScript são implementadas difere de navegador para navegador, mas geralmente recorrem a uma implementação esparsa - provavelmente a mesma usada para acesso de propriedade de objetos regulares - se usar uma matriz real seria ineficiente.

Você terá que perguntar a alguém com mais conhecimento sobre implementações específicas para responder o que exatamente desencadeia a mudança de denso para esparso, mas seu exemplo deve ser perfeitamente seguro. Se você deseja obter uma matriz densa, deve chamar o construtor com um argumento de comprimento explícito e esperar que realmente obtenha um.

Veja esta resposta para uma descrição mais detalhada de olliej.

Christoph
fonte
1
Não acho que você realmente obtenha uma matriz densa se disser algo como foo = new Array(10000). No entanto, este é suposto trabalho: foo = Array.apply(null, {length: 10});.
doubleOrt
70

Sim, eles estão. Na verdade, elas são tabelas hash internamente, portanto, você pode usar não apenas números inteiros grandes, mas também strings, flutuantes ou outros objetos. Todas as chaves são convertidas em strings toString()antes de serem adicionadas ao hash. Você pode confirmar isso com algum código de teste:

<script>
  var array = [];
  array[0] = "zero";
  array[new Date().getTime()] = "now";
  array[3.14] = "pi";

  for (var i in array) {
      alert("array["+i+"] = " + array[i] + ", typeof("+i+") == " + typeof(i));
  }
</script>

Monitores:

array[0] = zero, typeof(0) == string
array[1254503972355] = now, typeof(1254503972355) == string
array[3.14] = pi, typeof(3.14) == string

Observe como usei a for...insintaxe, que fornece apenas os índices que estão realmente definidos. Se você usar o for (var i = 0; i < array.length; ++i)estilo mais comum de iteração, obviamente terá problemas com índices de array não padrão.

John Kugelman
fonte
9
a maioria das implementações JS armazenam propriedades indexadas numericamente em uma matriz real, se possível; isso é magia dos bastidores, porém: do ponto de vista da linguagem, matrizes são objetos regulares com uma lengthpropriedade mágica
Christoph,
7
@John: lengthsó é invisível em for..inloops porque tem o DontEnumsinalizador definido; no ES5, o atributo de propriedade é chamado enumerablee pode ser explicitamente definido viaObject.defineProperty()
Christoph,
14
Todas as chaves de objeto em JavaScript são sempre String; qualquer outra coisa que você colocar no subscrito será toString()-ed. Combine isso com a imprecisão de inteiro de grande número e isso significa que se você definir a[9999999999999999]=1, a[10000000000000000]será 1 (e muitos outros comportamentos surpreendentes). Usar números não inteiros como chaves é muito imprudente e objetos arbitrários estão fora de questão.
bobince,
71
Então tu deverás apenas usar Strings como chaves de objeto, nem mais, nem menos. String deve ser o tipo que você usará e o tipo de chave será String. Inteiro não usarás, nem usarás não-inteiros, exceto que tu então procedes ao cast para String. Objetos arbitrários estão certos.
Crescent Fresh,
8
Os índices da matriz devem ser inteiros. array [3.14] = pi funciona porque Array é inerente ao objeto. Exemplo: var x = []; x [.1] = 5; Então x tem um comprimento de 0 ainda.
Mike Blandford,
10

Você pode evitar o problema usando uma sintaxe javascript projetada para esse tipo de coisa. Você pode tratá-lo como um dicionário, mas a sintaxe "para ... em ..." permitirá que você pegue todos eles.

var sparse = {}; // not []
sparse["whatever"] = "something";
John Fisher
fonte
7

Os objetos Javascript são esparsos e os arrays são apenas objetos especializados com uma propriedade de comprimento mantida automaticamente (que na verdade é maior que o maior índice, não o número de elementos definidos) e alguns métodos adicionais. Você está seguro de qualquer maneira; use um array se precisar de seus recursos extras e um objeto caso contrário.

Apenas apaixonado
fonte
4
isso é do ponto de vista da linguagem; implementações realmente usam matrizes reais para armazenar propriedades numéricas densas
Christoph,
6

A resposta, como geralmente é verdade com JavaScript, é "é um pouco mais estranho ..."

O uso de memória não é definido e qualquer implementação pode ser estúpida. Em teoria, const a = []; a[1000000]=0;poderia queimar megabytes de memória, como poderia const a = [];. Na prática, até a Microsoft evita essas implementações.

Justin Love aponta que o atributo de comprimento é o conjunto de índice mais alto . MAS só é atualizado se o índice for um inteiro.

Portanto, a matriz é esparsa. MAS funções embutidas como reduzir (), Math.max () e "for ... of" percorrerão toda a gama de índices inteiros possíveis de 0 até o comprimento, visitando muitos que retornam 'indefinido'. MAS 'for ... in' loops podem fazer o que você espera, visitando apenas as chaves definidas.

Aqui está um exemplo usando Node.js:

"use strict";
const print = console.log;

let a = [0, 10];
// a[2] and a[3] skipped
a[4] = 40;
a[5] = undefined;  // which counts towards setting the length
a[31.4] = 'ten pi';  // doesn't count towards setting the length
a['pi'] = 3.14;
print(`a.length= :${a.length}:, a = :${a}:`);
print(`Math.max(...a) = :${Math.max(a)}: because of 'undefined values'`);
for (let v of a) print(`v of a; v=:${v}:`);
for (let i in a) print(`i in a; i=:${i}: a[i]=${a[i]}`);

dando:

a.length= :6:, a = :0,10,,,40,:
Math.max(...a) = :NaN: because of 'undefined values'
v of a; v=:0:
v of a; v=:10:
v of a; v=:undefined:
v of a; v=:undefined:
v of a; v=:40:
v of a; v=:undefined:
i in a; i=:0: a[i]=0
i in a; i=:1: a[i]=10
i in a; i=:4: a[i]=40
i in a; i=:5: a[i]=undefined
i in a; i=:31.4: a[i]=ten pi
i in a; i=:pi: a[i]=3.14

Mas. Existem mais casos esquivos com Arrays ainda não mencionados.

Charles Merriam
fonte
2

A escassez (ou densidade) pode ser confirmada empiricamente para NodeJS com o não padrão process.memoryUsage () .

Às vezes, o node é inteligente o suficiente para manter o array esparso:

Welcome to Node.js v12.15.0.
Type ".help" for more information.
> console.log(`The script is using approximately ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024 * 100) / 100} MB`)
The script is using approximately 3.07 MB
undefined
> array = []
[]
> array[2**24] = 2**24
16777216
> array
[ <16777216 empty items>, 16777216 ]
> console.log(`The script is using approximately ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024 * 100) / 100} MB`)
The script is using approximately 2.8 MB
undefined

Às vezes, o nó opta por torná-lo denso (esse comportamento pode muito bem ser otimizado no futuro):

> otherArray = Array(2**24)
[ <16777216 empty items> ]
> console.log(`The script is using approximately ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024 * 100) / 100} MB`)
The script is using approximately 130.57 MB
undefined

Em seguida, esparso novamente:

> yetAnotherArray = Array(2**32-1)
[ <4294967295 empty items> ]
> console.log(`The script is using approximately ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024 * 100) / 100} MB`)
The script is using approximately 130.68 MB
undefined

Portanto, talvez o uso de uma matriz densa para ter uma ideia do bug do kernel AIX original precise ser forçado com um intervalo semelhante :

> denseArray = [...Array(2**24).keys()]
[
   0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11,
  12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
  24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35,
  36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
  48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
  60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71,
  72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83,
  84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95,
  96, 97, 98, 99,
  ... 16777116 more items
]
> console.log(`The script is using approximately ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024 * 100) / 100} MB`);
The script is using approximately 819.94 MB
undefined

Porque por que não fazer cair?

> tooDenseArray = [...Array(2**32-1).keys()]

<--- Last few GCs --->

[60109:0x1028ca000]   171407 ms: Scavenge 1072.7 (1090.0) -> 1056.7 (1090.0) MB, 0.2 / 0.0 ms  (average mu = 0.968, current mu = 0.832) allocation failure 
[60109:0x1028ca000]   171420 ms: Scavenge 1072.7 (1090.0) -> 1056.7 (1090.0) MB, 0.2 / 0.0 ms  (average mu = 0.968, current mu = 0.832) allocation failure 
[60109:0x1028ca000]   171434 ms: Scavenge 1072.7 (1090.0) -> 1056.7 (1090.0) MB, 0.2 / 0.0 ms  (average mu = 0.968, current mu = 0.832) allocation failure 


<--- JS stacktrace --->

==== JS stack trace =========================================

    0: ExitFrame [pc: 0x100931399]
    1: StubFrame [pc: 0x1008ee227]
    2: StubFrame [pc: 0x100996051]
Security context: 0x1043830808a1 <JSObject>
    3: /* anonymous */ [0x1043830b6919] [repl:1] [bytecode=0x1043830b6841 offset=28](this=0x104306fc2261 <JSGlobal Object>)
    4: InternalFrame [pc: 0x1008aefdd]
    5: EntryFrame [pc: 0x1008aedb8]
    6: builtin exit frame: runInThisContext(this=0x104387b8cac1 <ContextifyScript map = 0x1043...

FATAL ERROR: invalid array length Allocation failed - JavaScript heap out of memory

Writing Node.js report to file: report.20200220.220620.60109.0.001.json
Node.js report completed
 1: 0x10007f4b9 node::Abort() [/Users/pzrq/.nvm/versions/node/v12.15.0/bin/node]
 2: 0x10007f63d node::OnFatalError(char const*, char const*) [/Users/pzrq/.nvm/versions/node/v12.15.0/bin/node]
 3: 0x100176a27 v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [/Users/pzrq/.nvm/versions/node/v12.15.0/bin/node]
 4: 0x1001769c3 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [/Users/pzrq/.nvm/versions/node/v12.15.0/bin/node]
 5: 0x1002fab75 v8::internal::Heap::FatalProcessOutOfMemory(char const*) [/Users/pzrq/.nvm/versions/node/v12.15.0/bin/node]
 6: 0x1005f3e9b v8::internal::Runtime_FatalProcessOutOfMemoryInvalidArrayLength(int, unsigned long*, v8::internal::Isolate*) [/Users/pzrq/.nvm/versions/node/v12.15.0/bin/node]
 7: 0x100931399 Builtins_CEntry_Return1_DontSaveFPRegs_ArgvOnStack_NoBuiltinExit [/Users/pzrq/.nvm/versions/node/v12.15.0/bin/node]
 8: 0x1008ee227 Builtins_IterableToList [/Users/pzrq/.nvm/versions/node/v12.15.0/bin/node]
Abort trap: 6
pzrq
fonte
1
Legal, e estou meio surpreso que minha pergunta de dez anos ainda seja relevante!
Berry
1

Eles podem ser, mas nem sempre precisam ser, e podem ter um desempenho melhor quando não o são.

Aqui está uma discussão sobre como testar a dispersão do índice em uma instância de array: https://benmccormick.org/2018/06/19/code-golf-sparse-arrays/

O vencedor deste código de golfe (menos caracteres) é:

let isSparse = a => !!a.reduce(x=>x-1,a.length)

Basicamente percorrer a matriz para entradas indexadas enquanto diminui o valor do comprimento e retorna o !!booleano endurecido do resultado numérico falso / verdadeiro (se o acumulador for decrementado até zero, o índice é totalmente preenchido e não esparso). As advertências de Charles Merriam acima também devem ser consideradas e este código não as aborda, mas elas se aplicam a entradas de string com hash que podem acontecer ao atribuir elementos arr[var]= (something)onde var não é um inteiro.

Um motivo para se preocupar com a dispersão do índice são seus efeitos no desempenho, que podem diferir entre os mecanismos de script, há uma grande discussão sobre criação / inicialização de array: Qual é a diferença entre "Array ()" e "[]" ao declarar um JavaScript matriz?

Uma resposta recente a essa postagem tem um link para este mergulho profundo em como o V8 tenta otimizar arrays marcando-os para evitar (re) testes de características como dispersão: https://v8.dev/blog/elements-kinds . A postagem do blog é de setembro de 17 e o material está sujeito a algumas alterações, mas a análise das implicações para o desenvolvimento do dia-a-dia é útil e clara.

dkloke
fonte