Archive for the ‘performance’ tag
6 Methoden, ein Verzeichnis rekursiv zu scannen
Wer von euch kennt solchen Code:
if ($handle = opendir('/path/to/files')) {
while (false !== ($file = readdir($handle))) {
echo "$file\n";
}
closedir($handle);
}So wird es häufig gemacht, aber hier wird natürlich nur ein Verzeichnis durchsucht und keine Unterordner betrachtet. Und häufig schleichen sich dort auch Fehler ein, beispielsweise wenn man im obigen Beispiel nur
while ($file = readdir($handle)) {schreibt. Es gibt aber noch eine Menge anderer Möglichkeiten, die ich hier einmal vorstellen und vergleichen möchte. Danach stelle ich noch einige Testergebnisse vor, um die verschiedenen Möglichkeiten zu vergleichen sowie Vor- und Nachteile aufzuzeigen.
Hier erstmal das Script mit den einzelnen Funktionen:
$method = 'dirRead';
echo "mem before:".memory_get_peak_usage(true)."\n";
echo "Starting $method()\n";
clearstatcache();
$startTime = microtime(true);
$allFiles = DirectoryParser::$method('C:/Temp');
echo "Zeit: ".(microtime(true) - $startTime)." sec\n";
echo "mem after:".memory_get_peak_usage(true)."\n";
echo count($allFiles)."\n\n";
class DirectoryParser
{
public static function createTestFolders($dir) {
for($i=0; $i<10000; $i++) {
@mkdir($dir.'/'.$i);
for ($u=0; $u<10; $u++) {
touch($dir.'/'.$i.'/'.rand(10000, 999999).'.txt');
}
}
}
public static function dirRead($dir, &$fileinfo = array()) {
if ($handle = dir($dir)) {
while (false !== ($file = $handle->read())) {
if (!is_dir($dir.'/'.$file)) {
$fileinfo[] = array($file, filesize($dir.'/'.$file));
} elseif (is_dir($dir.'/'.$file) && $file != '.' && $file != '..') {
self::dirRead($dir.'/'.$file, $fileinfo);
}
}
$handle->close();
}
return $fileinfo;
}
public static function find($dir) {
if (substr(php_uname(), 0, 7) == "Windows") {
foreach (explode("\n",` cd "$dir" && dir /S /B /A-D `) as $fullFilename) {
if ($fullFilename != '') {
$fileinfo[] = array($fullFilename, filesize($fullFilename));
}
}
} else {
foreach (explode("\n",` cd $dir && find -maxdepth 3 -type f ! -name ".*" -printf "%f\r%s\n" `) as $fileInfos) {
if ($fileInfos != '') {
$fileinfo[] = explode("\r", $fileInfos);
}
}
}
return $fileinfo;
}
public static function glob($dir, &$fileinfo = array()) {
foreach (glob($dir.'/*') as $file) {
if (is_dir($file)) {
self::glob($file, $fileinfo);
} else {
$fileinfo[] = array(basename($file), filesize($file));
}
}
return $fileinfo;
}
public static function scandir($outerDir){
$dirs = array_diff(scandir($outerDir), array(".", ".."));
$fileArray = array();
foreach ($dirs as $d) {
if (is_dir($outerDir."/".$d)) {
$fileArray = array_merge($fileArray, self::scandir($outerDir."/".$d));
} else {
$fileArray[] = array($d, filesize($outerDir."/".$d));
}
}
return $fileArray;
}
public static function opendir($dir, &$fileinfo = array()) {
if ($handle = opendir($dir)) {
while (false !== ($file = readdir($handle))) {
if (!is_dir($dir.'/'.$file)) {
$fileinfo[] = array($file, filesize($dir.'/'.$file));
} elseif (is_dir($dir.'/'.$file) && $file != '.' && $file != '..') {
self::opendir($dir.'/'.$file, $fileinfo);
}
}
closedir($handle);
}
return $fileinfo;
}
public static function directoryIterator($dir) {
$iterator = new RecursiveDirectoryIterator($dir);
foreach(new RecursiveIteratorIterator($iterator, RecursiveIteratorIterator::CHILD_FIRST) as $file) {
if (false == $file->isDir()) {
$fileinfo[] = array($file->getFilename(), $file->getSize());
}
}
return $fileinfo;
}
}Man wählt also in Zeile 1 eine Methode, stellt in Zeile 7 den Pfad korrekt ein, und lässt das Script laufen. Falls man ein Testverzeichnis erstellen möchte mit 100.000 Dateien, kann man die Methode createTestFolders nutzen.
Bei allen Methoden, die Verzeichnisse durchsuchen und Dateien auslesen (z.B. um die Größe festzustellen) muss man darauf achten, nicht zuviele “offene Filehandles” zu haben, denn viele Betriebssysteme haben da festgelegte Grenzen, wieviele offene Dateien ein Prozess haben darf.
Unter Linux findet man das beispielsweise heraus mit
$ cat /proc/sys/fs/file-max 184665 $ sysctl fs.file-max fs.file-max = 184665
Leider weiß ich nicht, wie man die maximale Anzahl an gleichzeitigen FileDescriptors (“open files”) eines laufenden Prozesses herausfindet, deshalb habe ich in der unten stehenden Tabelle nur die Gesamtanzahl der Filesystem-Events unter Windows 7 rausgeschrieben (herausgefunden mit dem Process Monitor). Wieviele davon jeweils gleichzeitig geöffnet waren konnte ich nicht herausfinden. Wer weiß Bescheid?
Des weiteren habe ich die Laufzeit protokolliert und auch die Algorithmen einmal einzeln laufen lassen, um den maximalen Speicherverbrauch herauszufinden.
| Methode | Laufzeit (s) | MemoryPeak (MB) | FilesystemEvents Win7 | Probleme |
|---|---|---|---|---|
| dir()/read() | 51 | 33 | 1207556 | |
| find/dir | 46 | 39 | 805840 | |
| glob() | 56 | 34 | 900006 | findet keine Dateien, die mit . beginnen |
| scandir() | 312 | 39 | 1057467 | |
| opendir()/readdir() | 52 | 33 | 1206817 | |
| DirectoryIterator | 87 | 33 | 2146331 |
Die Messungen habe ich mit meiner normalen SATA2 Festplatte gemacht, das untersuchte Verzeichnis hat 90.499 Dateien, die meisten davon 0 Byte groß und insgesamt über 10078 Unterordner verteilt.
Interessanterweise sind die Ergebnisse unter Linux deutlich anders gewesen.
- Einmal habe ich einen Ubuntu 9.10 Server genommen, mit SSD, aber leider relativ schwacher CPU. PHP 5.2.10
| Methode | Laufzeit (s) | MemoryPeak (MB) |
|---|---|---|
| dir()/read() | 499 | 93 |
| find/dir | 135 | 109 |
| glob() | 510 | 95 |
| scandir() | 1043 | 113 |
| opendir()/readdir() | 507 | 94 |
| DirectoryIterator | 622 | 88 |
- Den selben Ubuntu Server, aber mit PHP 5.3.1
| Methode | Laufzeit (s) | MemoryPeak (MB) |
|---|---|---|
| dir()/read() | 160 | 56 |
| find/dir | 146 | 76 |
| glob() | 131 | 58 |
| scandir() | 2915 | 79 |
| opendir()/readdir() | 142 | 57 |
| DirectoryIterator | 156 | 57 |
- Einmal einen Debian-Server mit QuadCore, Raid1 SATA2 und PHP 5.2.11
| Methode | Laufzeit (s) | MemoryPeak (MB) |
|---|---|---|
| dir()/read() | 380 | 53 |
| find/dir | 2 | 64 |
| glob() | 349 | 55 |
| scandir() | 744 | 64 |
| opendir()/readdir() | 366 | 54 |
| DirectoryIterator | 3 | 46 |
Verstehen tue ich einige Ergebnisse noch nicht, zum Beispiel warum die find-Methode auf dem Debian-Server nur 2 Sekunden benötigt, wohingehen sie unter Ubuntu jeweils > 2 Minuten dauert. Das selbe beim DirectoryIterator. Komisch. Außerdem scheint PHP 5.3 ordentlich an Geschwindigkeit zugelegt zu haben gegenüber 5.2.
Falls jemand von euch weiß, wie man die Anzahl der FilesystemEvents (inotify?) unter Linux herausbekommen kann, oder die Anzahl der maximalen gleichzeitigen Filehandles (sowas wie lsof -p PID, nur das Maximum während eines Programmdurchlaufs), bitte in den Kommentaren melden.
Ich überlege auch, ob es sich lohnt, diese Tests mal auf einem NFS-Laufwerk im Netzwerk zu machen, das könnte man evtl. auch mal brauchen.
(Die Tabellen wollte ich übrigens mal ausprobieren, sind mit dem WordPress-Addon WP-Table Reloaded erstellt worden)
Echo, Print und die Parameter
Wenn man sehr viel mit Strings und Stringausgaben arbeitet, und dabei maximale Performance braucht, gibt es einige Tricks, die dabei helfen, die letzten Prozente rauszuholen.
Bedenkt also: Diese leichten Performance-Unterschiede bringen nur bei sehr großen Scripten etwas. Bei einer kleinen Webseite oder ähnlichem ist der Unterschied zu vernachlässigen, da zählt mehr die persönliche Vorliebe und Lesbarkeit.
Zuerst einmal: in PHP gibt es zur Ausgabe von Daten (das können ja auch Zahlen sein, nicht nur Strings) zwei einfache Funktionen: echo und print.
Diese beiden Funktionen sind etwas besonderes: Es sind Sprachkonstrukte, und keine Funktionen. Man kann sie also auch ohne Klammern benutzen, beispielsweise so:
echo 'Dies ist ein Text';
echo('Dies ist ein Text');Aber das kann auch zu Problemen führen, beispielsweise in diesem Quelltext:
(1 === $i) ? echo 'gleich' : echo 'ungleich';
Bei dieser trinären Bedingung funktioniert echo nicht, da es keinen Rückgabewert hat. print funktioniert.
echo ist print immer vorzuziehen. Warum? echo ist leicht performanter (aber nur im tausendstel Bereich, siehe unten). Außerdem bietet es die Möglichkeit, mehrere Parameter zu übergeben. Auch das ist minimal schneller als einen String mit Punkten zu verbinden.
In den folgenden Scripten nutze ich jeweils den Output Buffer von PHP, um nur im Speicher zu arbeiten. Wenn man die ob_-Funktionen weglassen würde, würde ein sehr großer Teil der CPU-Belastung (und damit des Tests) auf die eigentliche Ausgabe in der Console (bzw. dem Browser) draufgehen und die Ergebnisse verfälschen.
<?php
$string1 = "_string1_";
$string2 = "_string2_";
$timeStartComma = microtime(true);
for ($i=0; $i<1000000; $i++) {
ob_start();
echo $string1, 'sometexthere', $string2, "\r\n";
ob_get_clean();
}
$timeEndComma = microtime(true);
$timeStartDot = microtime(true);
for ($i=0; $i<1000000; $i++) {
ob_start();
echo $string1 . 'sometexthere' . $string2 . "\r\n";
ob_get_clean();
}
$timeEndDot = microtime(true);
$timeStartPrint = microtime(true);
for ($i=0; $i<1000000; $i++) {
ob_start();
print $string1 . 'sometexthere' . $string2 . "\r\n";
ob_get_clean();
}
$timeEndPrint = microtime(true);
echo "Comma needed " . ($timeEndComma-$timeStartComma) . " seconds\n";
echo "Dot needed " . ($timeEndDot-$timeStartDot) . " seconds\n";
echo "Print needed " . ($timeEndPrint-$timeStartPrint) . " seconds\n";
?><?php
$string1 = "_string1_";
$string2 = '_string2_';
$timeStartDouble = microtime(true);
for ($i=0; $i<1000000; $i++) {
ob_start();
echo "_string1__string1__string1__string1_";
ob_get_clean();
}
$timeEndDouble = microtime(true);
$timeStartSingle = microtime(true);
for ($i=0; $i<1000000; $i++) {
ob_start();
echo '_string2__string2__string2__string2_';
ob_get_clean();
}
$timeEndSingle = microtime(true);
echo "Double needed " . ($timeEndDouble-$timeStartDouble) . " seconds\n";
echo "Single needed " . ($timeEndSingle-$timeStartSingle) . " seconds\n";
?>Es ist übrigens minimal schneller, wenn man keine Klammern nutzt.
<?php
$timeStartWithout = microtime(true);
for ($i=0; $i<1000000; $i++) {
ob_start();
echo '_string1_';
ob_get_clean();
}
$timeEndWithout = microtime(true);
$timeStartWith = microtime(true);
for ($i=0; $i<1000000; $i++) {
ob_start();
echo('_string1_');
ob_get_clean();
}
$timeEndWith = microtime(true);
echo "Without brackets needed " . ($timeEndWithout-$timeStartWithout) . " seconds\n";
echo "With brackets needed " . ($timeEndWith-$timeStartWith) . " seconds\n";
?>Man beachte wie gesagt, dass wir hier von 1 Million Schleifendurchläufen reden. In 99,9% aller Fälle ist der Unterschied zu vernachlässigen. Wer also hoch performante Riesenscripte bauen möchte, sollte sich das ganze genau anschauen (oder solche Dinge gleich in C schreiben, aber ich will ja keine Werbung für andere Programmiersprachen machen
)


