Sind Javascript-Arrays spärlich?

97

Das heißt, wenn ich die aktuelle Zeit als Index für das Array verwende:

array[Date.getTime()] = value;

Wird der Interpreter alle Elemente von 0 bis jetzt instanziieren? Machen es verschiedene Browser anders?

Ich erinnere mich, dass es früher einen Fehler im AIX- Kernel gab, der auf Anfrage Pseudo-ttys erzeugte, aber wenn Sie "echo> / dev / pty10000000000" sagten, wurde / dev / pty0, / dev / pty1, .... und dann tot umfallen. Auf Messen hat es Spaß gemacht, aber ich möchte nicht, dass dies meinen Kunden passiert.

Beere
quelle
1
Ein möglicher Nachteil dabei ist die Schwierigkeit des Debuggens in Firebug. Eine Protokollanweisung im Array listet nur die ersten 1000 Elemente im Array auf, die alle "undefiniert" sind. Außerdem zeigt array.length an, dass Ihr Array n Elemente enthält, obwohl n-1 nur undefinierte "Geister" -Werte sind.
Michael Butler
Das Debuggen in Chrome ist jetzt in Ordnung - hier ein Beispiel für die Konsolenausgabe: [leer × 9564, Objekt, leer × 105, Objekt, leer × 10, Objekt, leer × 12, Objekt, leer × 9, Objekt, leer × 21, Objekt, leer × 9, Objekt]
jsalvata

Antworten:

40

Wie genau JavaScript-Arrays implementiert werden, ist von Browser zu Browser unterschiedlich, sie greifen jedoch im Allgemeinen auf eine spärliche Implementierung zurück - höchstwahrscheinlich dieselbe, die für den Eigenschaftszugriff auf reguläre Objekte verwendet wird -, wenn die Verwendung eines tatsächlichen Arrays ineffizient wäre.

Sie müssen jemanden mit mehr Wissen über bestimmte Implementierungen fragen, um zu beantworten, was genau die Verschiebung von dicht zu dünn auslöst, aber Ihr Beispiel sollte absolut sicher sein. Wenn Sie ein dichtes Array erhalten möchten, sollten Sie den Konstruktor mit einem expliziten Längenargument aufrufen und hoffen, dass Sie tatsächlich eines erhalten.

In dieser Antwort finden Sie eine detailliertere Beschreibung von olliej.

Christoph
quelle
1
Ich glaube nicht, dass Sie tatsächlich ein dichtes Array erhalten, wenn Sie so etwas sagen foo = new Array(10000). Dies soll jedoch funktionieren : foo = Array.apply(null, {length: 10});.
DoubleOrt
70

Ja, sind Sie. Sie sind intern tatsächlich Hash-Tabellen, sodass Sie nicht nur große Ganzzahlen, sondern auch Zeichenfolgen, Gleitkommazahlen oder andere Objekte verwenden können. Alle Schlüssel werden über in Zeichenfolgen konvertiert, toString()bevor sie dem Hash hinzugefügt werden. Sie können dies mit einem Testcode bestätigen:

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

Anzeigen:

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

Beachten Sie, wie ich die for...inSyntax verwendet habe, die nur die tatsächlich definierten Indizes enthält. Wenn Sie den allgemeineren for (var i = 0; i < array.length; ++i)Iterationsstil verwenden, treten offensichtlich Probleme mit nicht standardmäßigen Array-Indizes auf.

John Kugelman
quelle
9
Die meisten JS-Implementierungen speichern nach Möglichkeit numerisch indizierte Eigenschaften in einem tatsächlichen Array. Das ist jedoch Magie hinter den Kulissen: Aus sprachlicher Sicht sind Arrays reguläre Objekte mit einer magischen lengthEigenschaft
Christoph
7
@ John: lengthist nur in for..inSchleifen unsichtbar, weil das DontEnumFlag gesetzt ist; In ES5 wird das Eigenschaftsattribut aufgerufen enumerableund kann explizit überObject.defineProperty()
Christoph
14
Alle Objektschlüssel in JavaScript sind immer String; Alles andere, was Sie in den Index toString()eingeben, wird -ed. Kombinieren Sie dies mit der ganzzahligen Ungenauigkeit einer großen Zahl und es bedeutet, wenn Sie festlegen a[9999999999999999]=1, a[10000000000000000]wird 1 (und viele weitere überraschende Verhaltensweisen) sein. Die Verwendung von Nicht-Ganzzahlen als Schlüssel ist sehr unklug, und beliebige Objekte sind direkt verfügbar.
Bobince
71
Dann sollst du nur Strings als Objektschlüssel verwenden, nicht mehr und nicht weniger. String soll der Typ sein, den du verwenden sollst, und der Typ des Schlüssels soll String sein. Ganzzahlen sollst du nicht verwenden, und du sollst auch keine Ganzzahlen verwenden, außer dass du dann fortfährst, in String umzuwandeln. Beliebige Objekte sind richtig.
Crescent Fresh
8
Array-Indizes müssen Ganzzahlen sein. array [3.14] = pi funktioniert, weil Array von Object erbt. Beispiel: var x = []; x [.1] = 5; Dann hat x noch eine Länge von 0.
Mike Blandford
10

Sie können das Problem vermeiden, indem Sie eine Javascript-Syntax verwenden, die für diese Art von Dingen entwickelt wurde. Sie können es als Wörterbuch behandeln, aber mit der Syntax "für ... in ..." können Sie sie alle erfassen.

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

Javascript-Objekte sind spärlich und Arrays sind nur spezialisierte Objekte mit einer automatisch gepflegten Längeneigenschaft (die tatsächlich größer ist als der größte Index, nicht die Anzahl der definierten Elemente) und einigen zusätzlichen Methoden. Sie sind so oder so sicher; Verwenden Sie ein Array, wenn Sie zusätzliche Funktionen benötigen, und ansonsten ein Objekt.

Nur verliebt
quelle
4
das ist aus sprachlicher Sicht; Implementierungen verwenden tatsächlich reale Arrays, um dichte numerische Eigenschaften zu speichern
Christoph
6

Die Antwort lautet, wie es normalerweise bei JavaScript der Fall ist, "es ist ein bisschen seltsamer ...".

Die Speichernutzung ist nicht definiert und jede Implementierung darf dumm sein. Theoretisch const a = []; a[1000000]=0;könnte Megabyte Speicher brennen, wie könnte const a = [];. In der Praxis vermeidet sogar Microsoft diese Implementierungen.

Justin Love weist darauf hin, dass das Längenattribut der höchste Indexsatz ist. ABER es wird nur aktualisiert, wenn der Index eine Ganzzahl ist.

Das Array ist also spärlich. ABER integrierte Funktionen wie redu (), Math.max () und "for ... of" durchlaufen den gesamten Bereich möglicher ganzzahliger Indizes von 0 bis zur Länge und besuchen viele, die "undefiniert" zurückgeben. ABER 'for ... in'-Schleifen können wie erwartet funktionieren und nur die definierten Schlüssel besuchen.

Hier ist ein Beispiel mit 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]}`);

Geben:

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

Aber. Es gibt weitere Eckfälle mit Arrays, die noch nicht erwähnt wurden.

Charles Merriam
quelle
2

Die Sparseness (oder Dichte) kann für NodeJS mit dem nicht standardmäßigen process.memoryUsage () empirisch bestätigt werden .

Manchmal ist der Knoten klug genug, um das Array spärlich zu halten:

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

Manchmal entscheidet sich der Knoten dafür, ihn dicht zu machen (dieses Verhalten könnte in Zukunft möglicherweise optimiert werden):

> 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

Dann wieder spärlich:

> 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

Vielleicht muss die Verwendung eines dichten Arrays, um ein Gefühl für den ursprünglichen AIX-Kernel-Fehler zu bekommen, mit einem erzwungen werden Range-Alike werden :

> 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

Denn warum nicht umfallen lassen?

> 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
quelle
1
Schön, und ich bin ein bisschen erstaunt, dass meine zehnjährige Frage immer noch relevant ist!
Berry
1

Sie können es sein, aber sie müssen es nicht immer sein, und sie können bessere Leistungen erbringen, wenn sie es nicht sind.

Hier ist eine Diskussion darüber, wie eine Indexinstanz in einer Array-Instanz getestet wird: https://benmccormick.org/2018/06/19/code-golf-sparse-arrays/

Dieser Code Golf Gewinner (wenige Zeichen) ist:

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

Grundsätzlich wird das Array nach indizierten Einträgen !!durchsucht, während der Längenwert dekrementiert und der gehärtete Boolesche Wert des falschen / wahrheitsgemäßen numerischen Ergebnisses zurückgegeben wird (wenn der Akkumulator vollständig auf Null dekrementiert wird, ist der Index vollständig gefüllt und nicht dünn). Die oben genannten Einschränkungen von Charles Merriam sollten ebenfalls berücksichtigt werden, und dieser Code behandelt sie nicht. Sie gelten jedoch für Hash-Zeichenfolgeneinträge, die auftreten können, wenn Elemente zugewiesen werden, bei arr[var]= (something)denen var keine Ganzzahl war.

Grund für die Sorge um die Index-Spärlichkeit sind die Auswirkungen auf die Leistung, die zwischen den Skript-Engines unterschiedlich sein können. Hier wird die Erstellung / Initialisierung von Arrays ausführlich diskutiert: Was ist der Unterschied zwischen "Array ()" und "[]" beim Deklarieren eines JavaScript ? Array?

Eine aktuelle Antwort auf diesen Beitrag enthält einen Link zu diesem tiefen Einblick, wie V8 versucht, Arrays durch Markieren zu optimieren, um (erneutes) Testen auf Eigenschaften wie Spärlichkeit zu vermeiden: https://v8.dev/blog/elements-kinds . Der Blog-Beitrag stammt aus dem 17. September und das Material unterliegt einigen Änderungen, aber die Aufschlüsselung nach Auswirkungen auf die tägliche Entwicklung ist nützlich und klar.

dkloke
quelle