Benchmarking listing files in a local directory in PHP

Published by on .

I benchmarked the common ways to list files in a local directory in PHP.

The methods I tested were scandir, readdir, glob, and DirectoryIterator.

As expected, runtime grows linearly with the number of directory entries.

Sorting accounts for a noticeable share of that runtime. scandir("", SCANDIR_SORT_NONE) and glob("*", GLOB_NOSORT") are both at least 10-30% faster than their sorted versions, which are the default.

scandir("", SCANDIR_SORT_NONE) is consistently the fastest option, with readdir usually coming in second.

DirectoryIterator is the slowest option for directories with fewer than 2000 files. Its relative performance improves as the directory grows, until it is about as fast as readdir and scandir for directories with 10000 files.

Below are the raw benchmark results and the code I used. Times are in ms and calculated as the average across 100 iterations.

Benchmark results

filesDirectoryIteratorreaddirscandirscandir unsortedglobglob unsorted
10.0080.0060.0060.0060.0060.005
100.0130.0090.0090.0080.0100.009
1000.0710.0410.0460.0390.0560.049
10000.6830.3760.4770.3570.5680.449
100003.9943.8725.2483.6126.2774.472

Benchmark code

<?php
namespace D;
class DirectoryReader
{
    public function __construct(
        protected string $directory,
    ) {
    }
    public function iterator($flags = 0)
    {
        $results = array();
        foreach (new \DirectoryIterator($this->directory) as $fileInfo) {
            $results[] = $fileInfo;
        }
        return $results;
    }
    public function readdir($flags = 0)
    {
        $results = array();
        if ($handle = opendir($this->directory)) {
            while (false !== ($filename = readdir($handle))) {
                $results[] = $filename;
            }
            closedir($handle);
        }
        return $results;
    }
    public function scandir($flags = 0)
    {
        return scandir($this->directory, $flags);
    }
    public function glob($flags = 0)
    {
        return glob($this->directory . '/*', $flags);
    }
}
if (empty($argv[1])) {
    echo "Usage: php " . basename(__FILE__) . " <directory>\n";
    exit(1);
}
$directory = $argv[1];
if (is_dir($directory)) {
    echo "Directory exists already\n";
    exit(1);
}
mkdir($directory, 0755);
$reader = new DirectoryReader($directory);
$n_iterations = 100;
$step_size = 10;
$tests = [
    ['iterator', 0],
    ['readdir', 0],
    ['scandir', 0],
    ['scandir', SCANDIR_SORT_NONE],
    ['glob', 0],
    ['glob', GLOB_NOSORT],
];
echo "files\t" . join("\t", array_map(function ($t) {
    $name = $t[0];
    if ($t[1]) {
        $name .= ' unsorted';
    }
    return $name;
}, $tests)) . "\n";
for ($n = 1; $n <= 10000; $n *= 10) {
    for ($f = (int) ($n / 10); $f <= $n; $f++) {
        touch("{$directory}/{$f}.txt");
    }
    $line = "{$n}\t";
    foreach ($tests as [$method, $flags]) {
        $start = microtime(true);
        for ($i = 0; $i < $n_iterations; $i++) {
            $reader->{$method}($flags);
        }
        $end = microtime(true);
        $time_per_it = sprintf('%.3f', ($end - $start) / $n_iterations * 1000);
        $line .= "$time_per_it\t";
    }
    $line .= "\n";
    echo $line;
}