Mit Hashmaps erstellte Sparsed-Arrays sind für häufig gelesene Daten sehr ineffizient. Die effizienteste Implementierung verwendet einen Trie, der den Zugriff auf einen einzelnen Vektor ermöglicht, in dem Segmente verteilt sind.
Ein Trie kann berechnen, ob ein Element in der Tabelle vorhanden ist, indem er nur eine schreibgeschützte ZWEI-Array-Indizierung durchführt, um die effektive Position zu ermitteln, an der ein Element gespeichert ist, oder um festzustellen, ob es im zugrunde liegenden Speicher nicht vorhanden ist.
Es kann auch eine Standardposition im Sicherungsspeicher für den Standardwert des sparsed Arrays bereitstellen, sodass Sie keinen Test für den zurückgegebenen Index benötigen, da der Trie garantiert, dass alle möglichen Quellindizes mindestens dem Standardwert zugeordnet werden Position im Hintergrundspeicher (wo Sie häufig eine Null, eine leere Zeichenfolge oder ein Nullobjekt speichern).
Es gibt Implementierungen, die schnell aktualisierbare Versuche unterstützen, mit einer optionalen "compact ()" - Operation, um die Größe des Sicherungsspeichers am Ende mehrerer Operationen zu optimieren. Versuche sind VIEL schneller als Hashmaps, da sie keine komplexe Hashing-Funktion benötigen und keine Kollisionen für Lesevorgänge verarbeiten müssen (Bei Hashmaps haben Sie Kollisionen BEIDES zum Lesen und Schreiben, dies erfordert eine Schleife, um zum zu springen nächste Kandidatenposition und ein Test für jeden von ihnen, um den effektiven Quellindex zu vergleichen ...)
Darüber hinaus kann Java Hashmaps nur Objekte indizieren, und das Erstellen eines Integer-Objekts für jeden Hash-Quellindex (diese Objekterstellung wird für jeden Lesevorgang und nicht nur für Schreibvorgänge benötigt) ist im Hinblick auf Speicheroperationen kostspielig, da dies den Garbage Collector belastet .
Ich hatte wirklich gehofft, dass die JRE eine IntegerTrieMap <Object> als Standardimplementierung für die langsame HashMap <Integer, Object> oder LongTrieMap <Object> als Standardimplementierung für die noch langsamere HashMap <Long, Object> enthält immer noch nicht der Fall.
Sie fragen sich vielleicht, was ein Trie ist?
Es ist nur ein kleines Array von Ganzzahlen (in einem kleineren Bereich als der gesamte Koordinatenbereich für Ihre Matrix), mit dem die Koordinaten einer ganzzahligen Position in einem Vektor zugeordnet werden können.
Angenommen, Sie möchten eine 1024 * 1024-Matrix, die nur wenige Werte ungleich Null enthält. Anstatt diese Matrix in einem Array mit 1024 * 1024 Elementen (mehr als 1 Million) zu speichern, möchten Sie sie möglicherweise nur in Unterbereiche der Größe 16 * 16 aufteilen, und Sie benötigen nur 64 * 64 solcher Unterbereiche.
In diesem Fall enthält der Trie-Index nur 64 * 64 Ganzzahlen (4096), und es gibt mindestens 16 * 16 Datenelemente (die die Standardnullen oder den häufigsten Teilbereich in Ihrer Sparse-Matrix enthalten).
Und der Vektor, der zum Speichern der Werte verwendet wird, enthält nur 1 Kopie für Unterbereiche, die gleich sind (die meisten von ihnen sind voller Nullen und werden durch denselben Unterbereich dargestellt).
Anstatt eine Syntax wie zu verwenden matrix[i][j]
, würden Sie eine Syntax wie: verwenden.
trie.values[trie.subrangePositions[(i & ~15) + (j >> 4)] +
((i & 15) << 4) + (j & 15)]
Dies wird bequemer mit einer Zugriffsmethode für das Trie-Objekt gehandhabt.
Hier ist ein Beispiel, das in eine kommentierte Klasse integriert ist (ich hoffe, es wird OK kompiliert, da es vereinfacht wurde; signalisieren Sie mir, wenn es Fehler zu korrigieren gibt):
public class DoubleTrie {
public static final int SIZE_I = 1024;
public static final int SIZE_J = 1024;
public static final double DEFAULT_VALUE = 0.0;
private static final int SUBRANGEBITS_I = 4;
private static final int SUBRANGEBITS_J = 4;
private static final int SUBRANGE_I =
1 << SUBRANGEBITS_I;
private static final int SUBRANGE_J =
1 << SUBRANGEBITS_J;
private static final int SUBRANGEMASK_I =
SUBRANGE_I - 1;
private static final int SUBRANGEMASK_J =
SUBRANGE_J - 1;
private static final int SUBRANGE_POSITIONS =
SUBRANGE_I * SUBRANGE_J;
private static final int SUBRANGES_I =
(SIZE_I + SUBRANGE_I - 1) / SUBRANGE_I;
private static final int SUBRANGES_J =
(SIZE_J + SUBRANGE_J - 1) / SUBRANGE_J;
private static final int SUBRANGES =
SUBRANGES_I * SUBRANGES_J;
private static final int DEFAULT_POSITIONS[] =
new int[SUBRANGES](0);
private static final double DEFAULT_VALUES[] =
new double[SUBRANGE_POSITIONS](DEFAULT_VALUE);
private static final int subrangeOf(
final int i, final int j) {
return (i >> SUBRANGEBITS_I) * SUBRANGE_J +
(j >> SUBRANGEBITS_J);
}
private static final int positionOffsetOf(
final int i, final int j) {
return (i & SUBRANGEMASK_I) * MAX_J +
(j & SUBRANGEMASK_J);
}
public static final int arraycompare(
final double[] values1, final int position1,
final double[] values2, final int position2,
final int length) {
if (position1 >= 0 && position2 >= 0 && length >= 0) {
while (length-- > 0) {
double value1, value2;
if ((value1 = values1[position1 + length]) !=
(value2 = values2[position2 + length])) {
if (value1 < value2)
return -1;
if (value1 > value2)
return 1;
if (value1 == value2)
return 0;
if (value1 == value1)
return -1;
if (value2 == value2)
return 1;
long raw1, raw2;
if ((raw1 = Double.doubleToRawLongBits(value1)) !=
(raw2 = Double.doubleToRawLongBits(value2)))
return raw1 < raw2 ? -1 : 1;
}
}
return 0;
}
throw new ArrayIndexOutOfBoundsException(
"The positions and length can't be negative");
}
public static final int arraycompare(
final double[] values,
final int position1, final int position2,
final int length) {
return arraycompare(values, position1, values, position2, length);
}
public static final boolean arrayequals(
final double[] values1, final int position1,
final double[] values2, final int position2,
final int length) {
return arraycompare(values1, position1, values2, position2, length) ==
0;
}
public static final boolean arrayequals(
final double[] values,
final int position1, final int position2,
final int length) {
return arrayequals(values, position1, values, position2, length);
}
public static final void arraycopy(
final double[] values,
final int srcPosition, final int dstPosition,
final int length) {
arraycopy(values, srcPosition, values, dstPosition, length);
}
public static final double[] arraysetlength(
double[] values,
final int newLength) {
final int oldLength =
values.length < newLength ? values.length : newLength;
System.arraycopy(values, 0, values = new double[newLength], 0,
oldLength);
return values;
}
private double values[];
private int subrangePositions[];
private bool isSharedValues;
private bool isSharedSubrangePositions;
private final reset(
final double[] values,
final int[] subrangePositions) {
this.isSharedValues =
(this.values = values) == DEFAULT_VALUES;
this.isSharedsubrangePositions =
(this.subrangePositions = subrangePositions) ==
DEFAULT_POSITIONS;
}
public reset(final double initialValue = DEFAULT_VALUE) {
reset(
(initialValue == DEFAULT_VALUE) ? DEFAULT_VALUES :
new double[SUBRANGE_POSITIONS](initialValue),
DEFAULT_POSITIONS);
}
public DoubleTrie(final double initialValue = DEFAULT_VALUE) {
this.reset(initialValue);
}
public static DoubleTrie DEFAULT_INSTANCE = new DoubleTrie();
public DoubleTrie(final DoubleTrie source) {
this.values = (this.isSharedValues =
source.isSharedValues) ?
source.values :
source.values.clone();
this.subrangePositions = (this.isSharedSubrangePositions =
source.isSharedSubrangePositions) ?
source.subrangePositions :
source.subrangePositions.clone());
}
public double getAt(final int i, final int j) {
return values[subrangePositions[subrangeOf(i, j)] +
positionOffsetOf(i, j)];
}
public double setAt(final int i, final int i, final double value) {
final int subrange = subrangeOf(i, j);
final int positionOffset = positionOffsetOf(i, j);
int subrangePosition, valuePosition;
if (Double.compare(
values[valuePosition =
(subrangePosition = subrangePositions[subrange]) +
positionOffset],
value) != 0) {
if (isSharedValues) {
values = values.clone();
isSharedValues = false;
}
for (int otherSubrange = subrangePositions.length;
--otherSubrange >= 0; ) {
if (otherSubrange != subrange)
continue;
int otherSubrangePosition;
if ((otherSubrangePosition =
subrangePositions[otherSubrange]) >=
valuePosition &&
otherSubrangePosition + SUBRANGE_POSITIONS <
valuePosition) {
if (isSharedSubrangePositions) {
subrangePositions = subrangePositions.clone();
isSharedSubrangePositions = false;
}
values = setlengh(
values,
(subrangePositions[subrange] =
subrangePositions = values.length) +
SUBRANGE_POSITIONS);
valuePosition = subrangePositions + positionOffset;
break;
}
}
values[valuePosition] = value;
}
}
return value;
}
public void compact() {
final int oldValuesLength = values.length;
int newValuesLength = 0;
for (int oldPosition = 0;
oldPosition < oldValuesLength;
oldPosition += SUBRANGE_POSITIONS) {
int oldPosition = positions[subrange];
bool commonSubrange = false;
for (int newPosition = newValuesLength;
(newPosition -= SUBRANGE_POSITIONS) >= 0; )
if (arrayequals(values, newPosition, oldPosition,
SUBRANGE_POSITIONS)) {
commonSubrange = true;
for (subrange = subrangePositions.length;
--subrange >= 0; )
if (subrangePositions[subrange] == oldPosition)
subrangePositions[subrange] = newPosition;
break;
}
if (!commonSubrange) {
if (!commonSubrange && oldPosition != newValuesLength) {
arraycopy(values, oldPosition, newValuesLength,
SUBRANGE_POSITIONS);
newValuesLength += SUBRANGE_POSITIONS;
}
}
}
if (newValuesLength < oldValuesLength) {
values = values.arraysetlength(newValuesLength);
isSharedValues = false;
}
}
}
Hinweis: Dieser Code ist nicht vollständig, da er eine einzelne Matrixgröße verarbeitet und sein Kompaktor nur gemeinsame Unterbereiche erkennt, ohne sie zu verschachteln.
Außerdem bestimmt der Code nicht, wo die beste Breite oder Höhe zum Aufteilen der Matrix in Unterbereiche (für x- oder y-Koordinaten) entsprechend der Matrixgröße verwendet werden kann. Es werden nur die gleichen statischen Unterbereichsgrößen von 16 (für beide Koordinaten) verwendet, es kann jedoch auch jede andere kleine Potenz von 2 (aber eine Nicht-Potenz von 2 würde die int indexOf(int, int)
und int offsetOf(int, int)
interne Methoden verlangsamen ), unabhängig für beide Koordinaten und höher auf die maximale Breite oder Höhe der Matrix. Idealerweise sollte die compact()
Methode in der Lage sein, die besten Anpassungsgrößen zu bestimmen.
Wenn diese Aufspaltung Teilbereiche Größen variieren können, dann wird es notwendig sein , um beispielsweise Mitglieder für diese subrange Größen anstelle der statischen hinzufügen SUBRANGE_POSITIONS
, und die statischen Methoden zu machen int subrangeOf(int i, int j)
und int positionOffsetOf(int i, int j)
in nicht statisch; und die Initialisierungsarrays DEFAULT_POSITIONS
und DEFAULT_VALUES
müssen anders gelöscht oder neu definiert werden.
Wenn Sie Interleaving unterstützen möchten, teilen Sie zunächst die vorhandenen Werte in zwei Werte mit ungefähr derselben Größe (beide sind ein Vielfaches der minimalen Teilbereichsgröße, wobei die erste Teilmenge möglicherweise einen Teilbereich mehr als die zweite hat) und Sie scannen die größere an allen aufeinander folgenden Positionen, um eine passende Verschachtelung zu finden. Dann werden Sie versuchen, diese Werte abzugleichen. Dann werden Sie eine Schleife durchlaufen, indem Sie die Teilmengen in zwei Hälften teilen (auch ein Vielfaches der minimalen Teilbereichsgröße) und erneut scannen, um diese Teilmengen abzugleichen (dies multipliziert die Anzahl der Teilmengen mit 2: Sie müssen sich fragen, ob sich die Teilmengen verdoppelt haben Die Größe des subrangePositions-Index ist den Wert im Vergleich zur vorhandenen Größe der Werte wert, um festzustellen, ob er eine effektive Komprimierung bietet (wenn nicht, hören Sie dort auf: Sie haben die optimale Teilbereichsgröße direkt aus dem Interleaving-Komprimierungsprozess ermittelt. In diesem Fall; Die Größe des Unterbereichs kann während der Komprimierung geändert werden.
Dieser Code zeigt jedoch, wie Sie Werte ungleich Null zuweisen und das data
Array für zusätzliche Unterbereiche (ungleich Null) neu zuweisen. Anschließend können Sie die Speicherung dieser Daten ( compact()
nachdem Zuweisungen mithilfe der setAt(int i, int j, double value)
Methode durchgeführt wurden) optimieren, wenn Duplikate vorhanden sind Unterbereiche, die innerhalb der Daten vereinheitlicht und an derselben Position im subrangePositions
Array neu indiziert werden können .
Auf jeden Fall werden dort alle Prinzipien eines Versuchs umgesetzt:
Es ist immer schneller (und kompakter im Speicher, was eine bessere Lokalität bedeutet), eine Matrix unter Verwendung eines einzelnen Vektors anstelle eines doppelt indizierten Arrays von Arrays (jedes einzeln zugewiesen) darzustellen. Die Verbesserung ist in der double getAt(int, int)
Methode sichtbar !
Sie sparen viel Platz, aber beim Zuweisen von Werten kann es einige Zeit dauern, neue Unterbereiche neu zuzuweisen. Aus diesem Grund sollten die Unterbereiche nicht zu klein sein, da sonst zu häufige Neuzuordnungen zum Einrichten Ihrer Matrix auftreten.
Es ist möglich, eine anfängliche große Matrix automatisch in eine kompaktere Matrix umzuwandeln, indem gemeinsame Unterbereiche erkannt werden. Eine typische Implementierung enthält dann eine Methode wie compact()
oben. Wenn der Zugriff auf get () jedoch sehr schnell und set () sehr schnell ist, kann compact () sehr langsam sein, wenn viele gemeinsame Unterbereiche komprimiert werden müssen (z. B. wenn eine große, nicht dünn besetzte, zufällig gefüllte Matrix mit sich selbst subtrahiert wird oder multiplizieren Sie es mit Null: In diesem Fall ist es einfacher und viel schneller, den Versuch zurückzusetzen, indem Sie einen neuen instanziieren und den alten löschen.
Gemeinsame Unterbereiche verwenden gemeinsamen Speicher in den Daten, daher müssen diese gemeinsam genutzten Daten schreibgeschützt sein. Wenn Sie einen einzelnen Wert ändern müssen, ohne den Rest der Matrix zu ändern, müssen Sie zunächst sicherstellen, dass im subrangePositions
Index nur einmal auf ihn verwiesen wird . Andernfalls müssen Sie einen neuen Unterbereich an einer beliebigen Stelle (bequemerweise am Ende) des values
Vektors zuweisen und dann die Position dieses neuen Unterbereichs im subrangePositions
Index speichern .
Beachten Sie, dass die generische Colt-Bibliothek, obwohl sie sehr gut ist, bei der Arbeit mit spärlichen Matrizen nicht so gut ist, da sie Hashing- (oder zeilenkomprimierte) Techniken verwendet, die die Unterstützung für Versuche vorerst nicht implementieren, obwohl es sich um eine handelt Hervorragende Optimierung, die sowohl platzsparend als auch zeitsparend ist, insbesondere für die häufigsten getAt () -Operationen.
Selbst die hier für Versuche beschriebene Operation setAt () spart viel Zeit (der Weg wird hier implementiert, dh ohne automatische Komprimierung nach dem Einstellen, die je nach Bedarf und geschätzter Zeit implementiert werden könnte, bei der die Komprimierung noch viel Speicherplatz sparen würde Zeitpreis): Die Zeitersparnis ist proportional zur Anzahl der Zellen in Unterbereichen, und die Platzersparnis ist umgekehrt proportional zur Anzahl der Zellen pro Unterbereich. Ein guter Kompromiss, wenn dann eine Unterbereichsgröße wie die Anzahl der Zellen pro Unterbereich verwendet wird, ist die Quadratwurzel der Gesamtzahl der Zellen in einer 2D-Matrix (bei der Arbeit mit 3D-Matrizen wäre dies eine Kubikwurzel).
Hashing-Techniken, die in Colt-Sparse-Matrix-Implementierungen verwendet werden, haben den Nachteil, dass sie viel Speicheraufwand verursachen und die Zugriffszeit aufgrund möglicher Kollisionen verlangsamen. Versuche können alle Kollisionen vermeiden und können dann garantieren, dass im schlimmsten Fall lineare O (n) -Zeit bis O (1) -Zeit eingespart wird, wobei (n) die Anzahl möglicher Kollisionen ist (was im Fall einer spärlichen Matrix der Fall sein kann) bis zu der Anzahl der Zellen mit nicht standardmäßigem Wert in der Matrix, dh bis zur Gesamtzahl der Größe der Matrix multipliziert mit einem Faktor, der proportional zum Hashing-Füllfaktor ist, für eine nicht spärliche (dh vollständige Matrix).
Die in Colt verwendeten RC-Techniken (zeilenkomprimiert) sind näher an Tries, aber dies ist zu einem anderen Preis, hier die verwendeten Komprimierungstechniken, die eine sehr langsame Zugriffszeit für die häufigsten schreibgeschützten get () -Operationen haben und sehr langsam sind Komprimierung für setAt () -Operationen. Darüber hinaus ist die verwendete Komprimierung nicht orthogonal, anders als in dieser Darstellung von Versuchen, bei denen die Orthogonalität erhalten bleibt. Versuche würden auch sein, diese Orthogonalität für verwandte Betrachtungsoperationen wie Schreiten, Transposition (betrachtet als eine auf ganzzahligen zyklischen modularen Operationen basierende Schrittoperation), Unterordnung (und Unterauswahl im Allgemeinen, einschließlich mit Sortieransichten) beizubehalten.
Ich hoffe nur, dass Colt in Zukunft aktualisiert wird, um eine andere Implementierung mit Tries zu implementieren (dh TrieSparseMatrix anstelle von nur HashSparseMatrix und RCSparseMatrix). Die Ideen sind in diesem Artikel.
Die Trove-Implementierung (basierend auf int-> int-Maps) basiert ebenfalls auf Hashing-Techniken, die der HashedSparseMatrix von Colt ähneln, dh sie haben die gleichen Unannehmlichkeiten. Versuche werden viel schneller sein, mit einem moderaten zusätzlichen Speicherplatzbedarf (aber dieser Speicherplatz kann optimiert werden und in einer verzögerten Zeit sogar noch besser als Trove und Colt werden, indem eine endgültige compact () -Ionenoperation für die resultierende Matrix / Test durchgeführt wird).
Hinweis: Diese Trie-Implementierung ist an einen bestimmten nativen Typ gebunden (hier doppelt). Dies ist freiwillig, da die generische Implementierung unter Verwendung von Boxtypen einen enormen Platzbedarf hat (und in der Zugriffszeit viel langsamer ist). Hier werden nur native eindimensionale Arrays von Double anstelle von generischem Vector verwendet. Aber es ist sicherlich auch möglich, eine generische Implementierung für Tries abzuleiten ... Leider erlaubt Java immer noch nicht, wirklich generische Klassen mit allen Vorteilen nativer Typen zu schreiben, außer durch das Schreiben mehrerer Implementierungen (für einen generischen Objekttyp oder für jede nativer Typ) und Bedienung all dieser Vorgänge über eine Typfabrik. Die Sprache sollte in der Lage sein, die nativen Implementierungen automatisch zu instanziieren und die Factory automatisch zu erstellen (im Moment ist dies selbst in Java 7 nicht der Fall, und dies ist etwas, wo.