PHP Array, Improved

Python kanns, Ruby kanns, Ja sogar Java kanns. Nur PHP nicht. Nun, das stimmt nicht so ganz, PHP kann es über Umwege doch. Wovon die Rede ist?

Objekte als Hashtabellen-Schlüssel verwenden

Wie der eine oder andere Anwender von PHP weiß, können assoziative Arrays nur Integer und Zeichenketten als Schlüssel verwenden. Das ist manchmal sehr schade und man muss auf andere Werkzeuge ausweichen, indem man sich z.B. mit array_unique() und ArrayAccess sowie Iterator selbst etwas zurecht schustert.
Es gibt allerdings auch eine relativ elegante Lösung für dieses Problem: Die SplObjectStorage Klasse
Mit dieser Klasse aus der PHP SPL ist es möglich eine Datenstruktur zu schaffen, die sich wie ein assoziatives Array verhält, zugleich jedoch auch Objekte als Schlüssel erlaubt. Ein kleines Beispiel:

 1, 'prop2' => 2);
$obj2 = (object) array('prop1' => 11, 'prop2' => 22);
$obj3 = (object) array('prop1' => 11, 'prop2' => 22);
$objStorage = new SplObjectStorage();
$objStorage[$obj1] = 3;
$objStorage[$obj2] = 33;
$objStorage->contains($obj1); // TRUE
$objStorage->contains($obj2); // TRUE
$objStorage->contains($obj3); // FALSE
echo $objStorage[$obj1]; // 3
echo $objStorage[$obj2]; // 33
echo $objStorage[$obj3]; // UnexpectedValueException

Zu beachten ist, dass man zwar über ein Exemplar der Klasse SplObjectStorage iterieren kann, mit der $x as $y => $z Syntax jedoch unerwartetes Verhalten auftritt. In diesem Beispiel ist nämlich nicht $y das Objekt, sondern $z. $y entspricht der internen Position des Iterators. Nutzt man hingegen die $x as $y Syntax, ist $y das Objekt und man muss über $x[$y] auf dessen Wert zugreifen.
Ich hoffe ich konnte einen hilfreichen, wenn auch kurzen Einblick in die Klasse SplObjectStorage bieten. Dies war jedoch wirklich nur ein kleiner Ausschnitt, mehr kann über die Dokumentation erfahren werden.

Johannes Meyer
Johannes Meyer
Developer

Johannes ist seit 2011 bei uns und hilft bei der Entwicklung zukünftiger Knüller (Icinga2, Icinga Web 2, ...) aus dem Hause NETWAYS.

Weekly Snap: Introducing Gunnar & Java Script Surprises

9 – 13 January our development team took the lead, with a staff introduction and surprising JavaScript lessons learned.
Jannis discovered why native browser methods don’t necessarily guarantee better JavaScript performance. While testing his new Java array features to benchmark against native methods in V8 (Chrome) and Spider Monkey (Firefox) he was met with a surprise. In V8 his code was just a little slower or sometimes a little faster, but in Spider Monkey it was significantly better. He put this down to Mozilla’s tracing engine, and noted the best performance came with methods that take arguments as functions eg. Array.map(), Array.filter(), Array.forEach(). Jannis shared his simulation for others to test against, and in spite of his surprising find – he cautioned web developers to consider the compatibility of new language features before implementing them in a productive environment.
Following on, our Application Developer Gunnar, introduced himself and his work at NETWAYS. This has included work on a robust, distributed “single sign on” application for a customer, the inGraph addon for Icinga/Nagios, as well as on IcingaMQ. Away from the office, Gunnar plays with new technologies – most recently SIP and video-streaming via IP multicast.

JavaScript Performance: Warum 'nativ' nicht immer besser ist.

Eigentlich wollte ich den heutigen Artikel neuen JavaScript Array-Features widmen, die am kommen oder schon da sind (forEach(), indexOf(), Iteratoren, Generatoren, etc.). Garnieren wollte ich das ganze mit ein paar Benchmarks, um bestenfalls zu zeigen dass die ‘nativen’ Arraymethoden der Browser meine selbstgeschriebenen Equivalente vor Neid erblassen lassen und was die Zukunft bringt. Doch das wäre wohl zu einfach gewesen…
Das Ergebnis war überraschend: Während meine Benchmarks im v8 (d.h. die JS-Engine von Chrome) zwar zeigten, dass manche meiner Methoden nur einen kleinen Tick langsamer waren (manche aber auch schneller), war es bei Spidermonkey (die Firefox engine) genau umgekehrt: Sobald dort die Tracing Engine aktiviert war (was Standard seit FF > 3.5 ist), lief mein eigener Code mit einer viel besseren Performance (z.T um den Faktor x6) als die ‘nativen’ SpiderMonkey-Methoden, die in C++ geschrieben waren.

Was komisch klingt liegt an der Art und Weise wie Fuchs optimiert: Mein eigener Code wird zur Laufzeit ge’traced’ (grob gesagt: Es wird verfolgt, ob die Typen der Variablen gleichbleibend sind und dementsprechend wenn möglich auf Typkonvertierungen und -überprüfungen verzichtet – wer Details will findet eine gute Einführung im Mozilla Blog ). Dementsprechend werden bei meinem selbstgeschriebenen Code weniger Operationen pro Durchgang benötigt. Bei den nativen Methoden ist dass anscheinend anders: Eine kleine Wanderung in den C++ Code von Spidermonkey zeigte, dass z.B. die Array.prototype.every() Methode Typen immer überprüft und konvertiert und ich schätze, dass hier derzeit kein Tracing stattfindet.
Die größten Performancesprünge lagen dabei interessanterweise bei Methoden, die Funktionen als Argumente nehmen, wie z.b. Array.map(), Array.filter(), Array.forEach().
Hier noch mein Benchmark zum selber Ausprobieren. Natürlich ist das immer ein Szenario und deckt damit nicht jeden Fall ab – ein anderer Benchmark kann ein völlig anderes Ergebnis liefern, ein neuerer Browser andere Werte, etc. Ich habe die Funktionen was Argumente und Verhalten angeht nachgeschrieben, allerdings ist ein 1:1 Vergleich immer schwer und das Szenario (wie bei einem Benchmark üblich) natürlich extrem (10000 Elemente im Array und 1000 Durchgänge).
Das eigentliche Fazit: Gerade bei neuen Methoden wie den JavaScript 1.6+ Array Funktionen, die nicht bei jedem Browser vorhanden sind sollte immer erst geprüft ob sich der Aufwand lohnt, oder ob man auf ‘konservative’ Methoden zurückgreift. Cross-Browser-Kompatibilität erfordert immer Mehraufwand, und dieser wird nur gerechtfertigt wenn unterm Strich ein Mehrwert herauskommt. Und darauf will ich hinaus: Wir Webentwickler werden zwar einige neue Features wie Iteratoren und Generatoren bekommen (Firefox unterstützt diese auch schon bereits) – trotzdem ergibt es oft Sinn, lieber auf Helferfunktionen von Core-Toolset wie Prototype, JQuery oder ExtJS zu setzen, die browserübergreifend die selben Funktionalitäten anbieten. Die ‘neueren’ Methoden sind  a) nicht in jedem Browser vorhanden, und b) evtl. sogar langsamer, oder nur minimal schneller – womit sie derzeit für den Produktiveinsatz ausscheiden.
Ich will hier natürlich keinen Fortschritt aufhalten: Es ist gut, dass sinnvolle Hilfsmethoden wie filter() oder map() Einzug in den Standard finden und ich werde sie sicher auch selbst nutzen wenn Sie verbreitet genug sind. Allerdings sollte man für den Produktiveinsatz eher mit geschärften Bewusstsein an neue (Sprach-)Features wagen.

Low-level memory access in JavaScript: Typed arrays.

Interessante, aber eher unbekannte (da sehr neue) Datentypen in JavaScript  sind der ArrayBuffer und Typed Arrays. Diese Datentypen wurden in erster Linie eingeführt um effektiv auf Binärdaten zugreifen zu können.
Grundlage für alles ist der ArrayBuffer, ein einfacher Datentyp, den man beim Erstellen mitteilt wie viele Bytes er belegen soll. Das besondere ist dass dieser ArrayBuffer an sich keine Möglichkeit besitzt gelesen oder beschrieben zu werden (auch wenn die Spezifikation einen Proposal für eine ‘splice’ Methode hat). Hier kommen die Typed Array Datentypen (oder wie in der Spec: Views) hinzu. Diese ermöglichen es dem Speicher eine logische Aufteilung zu geben, z.B. als ein Array von 32-Bit großen Floats im Falle von Float32Array:
var buffer = new ArrayBuffer(8); // 8 Byte im speicher reservieren
var floatBuffer = new Float32Array(buffer); //floatBuffer ist jetzt ein Array mit 2 elementen (8*8/32 = 2)
floatBuffer[0] = 0.255315; // ab hier gibt es kaum einen Unterschied zu einem normalen js array, ausser dass indexüberlaufe einfach ignoriert werden
floatBuffer[1] = 0.3535;

Der Nutzen ist, dass man mit JavaScript nun effektiv mit binären Daten arbeiten kann. Davor musste man Binärdaten als String darstellen, mit charCodeAt Byteweise auslesen und Datentypen wie Floats und Integers ebenfalls aus Byteblöcken zusammenbauen.
Mich hat allerdings eher interessiert, wie sich die Performance von ‘normalen’ Operationen auf Arrays verhält, wenn man das JavaScript Array Objekt mit dem ArrayBuffer austauscht. Mein Benchmark dafür ist sehr einfach:

for(var i=0;i<size;i++) {
    container[i] = Math.random();
}
var sum = 0;
for(var i=1;i<size;i++) {
sum += container[i];
}
var avg = sum/size;

Ich fülle hier einfach ein Array ‘container’ (welches wahlweise ein ArrayBuffer oder ein Array Objekt ist) mit Zufallswerten und berechne anschliessend den Durchschnitt. Das ganze habe ich auf meinem MacBook Pro 13″ (i5 2,3Ghz) mit ‘size’ von 21 bis 225 einmal im Chrome 14 getestet und einmal im Firefox 6. Die Ergebnisse in bunt (Die Elemente mit 0 ms habe ich mal herausgelassen):

Man sieht ganz klar, wie die Performance des internen Arrays ab ca. 65536 Elementen ausreisst (vor allem im Chrome), während das Float32Array recht linear skaliert. Bei sehr großen Datenmengen lohnt es sich also, mal ein Float32Array -wenn vorhanden- auszuprobieren. Einen Nachteil haben die BufferArray Objekte allerdings: Das erstellen ist aufwändiger als bei internen Arrays. Generell sollte man immer die Performance messen und schauen, ob man wirklich einen sinnvollen Geschwindigkeitsbonus durch Typed Arrays bekommt – mein Benchmark hat keinen Anspruch auf Allgemeingültigkeit.
Fazit: JavaScript kann jetzt endlich effektiv mit größeren und binären Datenmengen umgehen. Man muss zwar entscheiden, ob sich der Aufwand lohnt – immerhin muss man, will man kompatibel zu älteren Browsern bleiben, eine Fallback-Variante anbieten. In Kombination mit WebSockets und der File Api eröffnen sich aber ganz neue Möglichkeiten (wie z.B. pdf.js, das PDF direkt in JavaScript rendert).

Umfangreiche JavaScript Arrays durchsuchen

“Einfache” Suchalgorithmen gehen meist so vor, alle Elemente des Arrays zu durchlaufen, bis der gesuchte Wert gefunden ist. Bei großen Arrays, können durch andere Algorithmen, kleinere oder größere Geschwindigkeitsvorteile erzielt werden – abhängig von Größe des Feldes und Anzahl der Suchoperationen. Ein Beispiel dafür wäre die binäre Suche, bei der das Feld aber sortiert sein muss. Sie basiert auf einer einfachen Form des Schemas Teile und Herrsche. Einfach gesagt, halbiert sich die Länge des Suchbereiches so von Schritt zu Schritt und spätestens bei einer Suchbereichslänge von 1, ist das Element gefunden.

 Array.prototype.bsearch = function(value) { var high = this.length, low = -1, mid; while(high - low > 1) { if(this[mid = high + low >> 1] < value) { low = mid; } else { high = mid; } } return this[high] != value ? -1 : high; } 
Eric Lippmann
Eric Lippmann
Lead Senior Developer

Eric kam während seines ersten Lehrjahres zu NETWAYS und hat seine Ausbildung bereits 2011 sehr erfolgreich abgeschlossen. Seit Beginn arbeitet er in der Softwareentwicklung und dort an den unterschiedlichen NETWAYS Open Source Lösungen, insbesondere inGraph und im Icinga Team an Icinga Web. Darüber hinaus zeichnet er sich für viele Kundenentwicklungen in der Finanz- und Automobilbranche verantwortlich.