ZURÜCK

Performance Tests #3 - locales

Geschwindigkeitstests und Analyse von locale-abhängigen Funktionen in PHP

Datum: 2021-10, Tags: php, locale, performance, test, translit, number, currency, format

The sources are available on gitlab: https://gitlab.com/jlue/php-performance-tests/-/tree/master/test-3-locales.


In den letzten Performance Tests habe ich mit der Geschwindigkeit von Data Transfer Objects und der der Geschwindigkeit von Arrays im Vergleich zu ArrayObjects beschäftigt. Was noch in meinen letzten Projekten immer wieder aufkommt, sind PHP-Funktionen, die abhängig von anderen installierten Betriebssystem-Komponenten sind, konkret: Von den glibc locales abhängige Funktionen, insbesondere iconv() und die damit mögliche Transliteration:

$result = iconv('UTF-8', 'ASCII//TRANSLIT', $string);

Vorab möchte ich noch erwähnen, dass die Referenzen und Dokumentationen zu dem Thema Lokalisierung und Internationalisierung in PHP schnell entweder lückenhaft, oder ziemlich komplex und unübersichtlich werden. Erst einmal wichtig für meine Tests ist, dass die iconv-Funtionen, wie in der Dokumentation beschrieben, von der lokalen libiconv Installation abhängig sind. In PHP wird dafür das Paket php7-iconv zur Verfügung gestellt, das wiederum von der lokalen glibc Installation abhängig ist, insbesondere dem Paket glibc-locale. Konkret bedeutet das, dass man die iconv-Funktionen nur dann sinnvoll nutzen kann, wenn die benötigten locales installiert sind.

locales Installation

Erst einmal ist es notwendig herauszufinden, welche locales auf der aktuellen PHP Umgebung überhaupt verfügbar sind. Auf Shell-Ebene ist das einfach, der Befehl locale -a gibt eine Liste der verfügbaren locales zurück.

host:~/php-performance-tests/test-3-localeslocale -a
aa_DJ
aa_DJ.utf8
aa_ER
...
zh_TW
zh_TW.euctw
zh_TW.utf8
zu_ZA
zu_ZA.utf8

Das sieht gut aus, es sind offenbar alle oder zumindest sehr viele installiert.

In einem Docker-Container oder einer PHP Sandbox ist das jedoch eventuell nicht möglich und Funktionen wie exec, passthru und sogar phpinfo() sind ggf. deaktiviert. Leider gibt es in PHP eine Funktion wie getlocale() o.ä. nicht. Man kann jedoch herausfinden, ob das Umschalten in eine bestimmte locale funktioniert, indem man dem zweiten Argument von setlocale() eine 0 übergibt. Dann wird als return die aktuelle locale zurückgegeben, etwa so:

host:~/php-performance-tests/test-3-localesphp
<?php
setlocale(LC_ALL, 'fr_CH');
var_dump(setlocale(LC_ALL, 0));
string(5) "fr_CH"

Wenn nicht die gewünschte locale zurückgegeben wird, muss die Installation angepasst werden.

Verwendung der locales

In der Dokumentation von setlocale() wird beschrieben, welche PHP Funktionen die unterschiedlichen locale-Kategorien benutzen.

Das sind die Funktionen, die ich testen möchte.

Wahrscheinlich wird üblicherweise in einer etwas komplexeren PHP Anwendung einmal zu Beginn der Ausführung die benötigte locale mit setlocale(LC_ALL, ...) gesetzt, etwa auf Basis der Sprach- oder generell Ländereinstellung des aktuellen Benutzers. Es gibt jedoch auch Szenarien, in denen mehrere Spracheinstellungen innerhalb einer Ausführung benötigt werden, z.B. um URLs für mehrere Sprachen mit Hilfe der Transliteration von iconv() zu generieren. In dem Fall müsste man innerhalb der Ausführung per setlocale() die Umgebung umschalten. Ist das noch akzeptabel performant?

Noch ein Hinweis zum Umgang mit setlocale(): Das Setzen der locale wirkt sich Prozessweit aus, das kann zu unerwarteten Überschneidungen führen. Beispielsweise der folgende Code liefert ein wahrscheinlich ungewolltes Ergebnis:

test-locale-overwrite.php
<?php

class setlocaleTest
{
    public function __construct(string $localeName)
    {
        setlocale(LC_ALL, $localeName);
    }

    public function printLocale(): void
    {
        var_dump(setlocale(LC_ALL, '0'));
    }
}

$de = new setlocaleTest('de_DE');
$de->printLocale();

$en = new setlocaleTest('en_GB');
$en->printLocale();

$de->printLocale();
host:~/php-performance-tests/test-3-localesphp test-locale-overwrite.php
string(5) "de_DE"
string(5) "en_GB"
string(5) "en_GB"

Außerdem bietet PHP noch ein anderes Internationalisierungs-Paket: intl.

PHP intl

Das intl-Paket in PHP bietet ebenfalls umfangreiche Lokalisierungs- und Internationalisierungsmöglichkeiten. Laut Einführung basiert es jedoch nicht auf iconv und glibc-locale, sondern auf den International Components for Unicode: ICU. In der intl Bibliothek wird das Paket glibc-locale und die einzelnen locales nicht benötigt, dafür jedoch die Bibliothek libicu und die dazugehörigen "rule databases" libicu-ledata. Wie gesagt werden die Abhängigkeiten hier schnell sehr unübersichtlich und variieren je nach Linux-Distribution.

Die Ermittlung der aktuellen Sprach- bzw. Ländereinstellung erfolgt bei intl per Locale Klasse:

host:~/php-performance-tests/test-3-localesphp
<?php
Locale::setDefault('fr_CH');
var_dump(Locale::getDefault());
string(5) "fr_CH"

Dabei kann die aktuelle locale auch per setlocale() gesetzt werden, ein Grund mehr, das Performance-Verhalten näher zu untersuchen.

host:~/php-performance-tests/test-3-localesphp
<?php
setlocale(LC_ALL, 'en_US');
var_dump(Locale::getDefault());
string(5) "en_US"

Doch vorher noch einmal ein Vergleich der benötigten Komponenten und Abhängigkeiten für meine lokale Installation:

Paketgrößen php7-iconv php7-intl
PHP 47 KB 500 KB
glibc 6 MB 6 MB
glibc-locale 209 MB -
libicu + ledata - 57 MB
Gesamtgröße mit Abhängigkeiten 215 MB 63 MB

Test Szenario

Der Test soll ermitteln, wie sehr sich das Hin- und Herschalten der Locale-Systemkomponenten auf die Ausführungszeit auswirkt. Dazu werden die entsprechenden PHP Funktionen über viele Iterationen ausgeführt und die Zeit gemessen. Im Vergleich dazu wird die Zeit gemessen, wenn vor jeder Ausführung die Locale-Einstellung um- und hinterher wieder zurückgeschaltet wird. Das soll den Anwendungsfall simulieren, wenn innerhalb eines PHP Prozesses eine locale-abhängige Transformation, Formatierung oder Transliteration über mehrere locales ausgeführt werden soll.

Das ist erst einmal besonders relevant bei den iconv Funktionen, denn die arbeiten immer nur mit der voreingestellten locale. Bei den intl Funktionen ist das teilweise anders, wie auch das sich auswirkt, werden die Messungen zeigen.

Dies ist die Basis-Klasse für den Test, der vollständige Sourcecode für diesen Test liegt auf gitlab.com: https://gitlab.com/jlue/php-performance-tests/-/tree/master/test-3-locales

DataLocalizedTranslit.php
<?php

namespace LocalesTest;

require_once 'LocalizationInterface.php';

class DataLocalizedTranslit implements LocalizationInterface
{
    protected const LOCALE_FIELD = LC_CTYPE;

    /**
     * @param mixed $sample
     *
     * @return string
     */
    public function plainSample($sample): string
    {
        return iconv('UTF-8','ASCII//TRANSLIT', $sample);
    }

    /**
     * @param mixed $sample
     * @param string $localeName
     *
     * @return string
     */
    public function localizedSample($sample, string $localeName): string
    {
        $currentLocale = setlocale(self::LOCALE_FIELD, 0);

        setlocale(self::LOCALE_FIELD, $localeName);

        $result = iconv('UTF-8','ASCII//TRANSLIT', $sample);

        setlocale(self::LOCALE_FIELD, $currentLocale);

        return $result;
    }
}

Die Klasse hat über das LocalizationInterface zwei Funtionen: plainSample() für die Ausführung ohne, und localizedSample() für die Ausführung mit Umschaltung der locale.

Die Zeiten werden dann über die plain- und localized-Scripte gemessen. Diese unterscheiden sich nur in einem Punkt: In der Ausführung der plainSample() oder localizedSample() Methode. In dem Script wird vor den Iterationen die Systemumgebung auf die locale C gesetzt, damit werden praktisch alle Locale-Funktionen deaktiviert. Das entspricht dem Zustand, wenn zwar php7-iconv installiert ist, jedoch die einzelnen locales nicht. Das ist beispielsweise bei den Docker Images von Alpine der Fall. Für den Test benutzen wir einen String mit deutschen Umlauten und einem Währungszeichen.

index-plain-translit.php
<?php

namespace LocalesTest;

require_once 'DataLocalizedTranslit.php';

$iterations = intval($argv[1] ?? 0);

setlocale(LC_ALL, 'C');
$sample = 'äöü ÄÖÜ ß €';

$uut = new DataLocalizedTranslit();

$sampleResult = $uut->plainSample($sample);
echo sprintf("%s (%s): ", $iterations, $sampleResult);

$start = microtime(true);

for ($i = 0; $i < $iterations; $i++) {
    $uut->plainSample($sample);
}

$end = microtime(true);

echo sprintf("%01.3f s\n", ($end - $start));
index-localized-translit.php
<?php

namespace LocalesTest;

require_once 'DataLocalizedTranslit.php';

$iterations = intval($argv[1] ?? 0);

setlocale(LC_ALL, 'C');
$sample = 'äöü ÄÖÜ ß €';
$sampleLocaleName ='de_DE.UTF8';

$uut = new DataLocalizedTranslit();

$sampleResult = $uut->localizedSample($sample, $sampleLocaleName);
echo sprintf("%s (%s): ", $iterations, $sampleResult);

$start = microtime(true);

for ($i = 0; $i < $iterations; $i++) {
    $uut->localizedSample($sample, $sampleLocaleName);
}

$end = microtime(true);

echo sprintf("%01.3f s\n", ($end - $start));

Ausgeführt wird das ganze dann wie im README.md beschrieben:

host:~/php-performance-tests/test-3-localesfor i in 64000 128000 256000 512000 1024000; do php index-plain-translit.php $i ; done
64000 (??? ??? ss EUR): 0.177 s
128000 (??? ??? ss EUR): 0.344 s
256000 (??? ??? ss EUR): 0.708 s
512000 (??? ??? ss EUR): 1.381 s
1024000 (??? ??? ss EUR): 2.959 s
host:~/php-performance-tests/test-3-localesfor i in 64000 128000 256000 512000 1024000; do php index-localized-translit.php $i ; done
64000 (aeoeue AEOEUE ss EUR): 0.419 s
128000 (aeoeue AEOEUE ss EUR): 0.774 s
256000 (aeoeue AEOEUE ss EUR): 1.534 s
512000 (aeoeue AEOEUE ss EUR): 3.105 s
1024000 (aeoeue AEOEUE ss EUR): 6.143 s

Man sieht, dass unter der locale C keine Ersetzung der deutschen Sonderzeichen möglich ist, wohl aber bei Umschaltung auf eine deutsche locale. Wenn man den test mit Umschaltung auf die locale "en_GB" ausführt, erhält man die Überstzung "aou AOU ss EUR". Das ist der ganze Trick an der Sache und der Grund für diesen Artikel.

host:~/php-performance-tests/test-3-localesphp
<?php
setlocale(LC_ALL, 'en_GB');
var_dump(iconv('UTF-8','ASCII//TRANSLIT', 'äöü ÄÖÜ ß €'));
string(14) "aou AOU ss EUR"

Daran finde ich erst einmal bemerkenswert, dass das ß auch im Englischen als "ss" übersetzt wird, obwohl es dort gar nicht vorkommt. Und auch, dass das Euro-Zeichen als "EUR" übesetzt wird, denn ich denke es ist ein Symbol und kein Schriftzeichen. Trotzdem ist das für mich erst einmal das gewünschte Verhalten.

Noch eine Anmerkung: Die deutschen Umlaute und auch das ß sind Bestandteil des "Latin-1" Zeichensatzes ISO 8859-1. Das Euro-Zeichen ist es nicht, das wurde unter "Latin-9" oder ISO/IEC 8859-15 als 0xA4 eingeführt. In UTF-8 wird es als 0x80 geführt, auf der Position 0xA4 befindet sich das allgemeine Währungssymbol ¤, wie es auch in "Latin-1" geführt wird.

Gerade im Kontext von URLs finde ich es persönlich wichtig, dass Umlaute und andere Sonderzeichen in den entsprechenden Sprachen richtig über-, bzw. ersetzt werden. Im Deutschen ist das für mich klar zu erkennen, aber für andere Sprachen nicht. Ich vermute, dass z.B. in Französisch oder Polnisch die Akzente einfach weggelassen werden und diese Art von Sonderzeichen-Ersetzung wie im Deutschen in anderen Sprachen nicht vor kommt. Dazu nochmal mehr untern bei den Auswertungen.

Test locales

locales translit

Der erste Testfall ist die Formatierung von Sonderzeichen mittels iconv "TRANSLIT" wie oben beschrieben.

DataLocalizedTranslit.php
<?php
...
        $result = iconv('UTF-8','ASCII//TRANSLIT', $sample);
...

locales datetime

Übersetzung von Datumsformaten mittels strftime.

Hier wird ein Unix-Timestamp (nicht DateTime Objekt) mittels strftime() und dem Format '%A %x' in das Format "Wochentagsname" und "Bevorzugte Datumsdarstellung gemäß dem Gebietsschema, ohne Zeitangabe" ausgegeben.

DataLocalizedDateTime.php
<?php
...
        $result = strftime('%A %x', $sample);
...

locales decimal

Formatierung von Zahlen, bezüglich Tausender-Trennzeichen und Kommazeichen mittels number_format.

Dazu wird die Fließkommazahl 20210901.1050 so formatiert, dass auf zwei Nachkommastellen gerundet wird.

DataLocalizedNumeric.php
<?php
...
        $result = number_format($sample, $this->decimals, $this->decimal_separator, $this->thousands_separator);
...

locales currency

Formatierung von Zahlen zur Währungsausgabe mittels money_format.

Dazu wird wiederum die Fließkommazahl 20210901.1050 benutzt und mit dem Format %i "gemäß des internationalen Währungsformats der Locale formatiert". Dabei wird wiederum eine Rundung anfallen.

DataLocalizedMoney.php
<?php
...
        $result = money_format('%i', $sample);
...

Test intl

Die Funktion money_format ist seit PHP 7.4 als DEPRECATED markiert und ab PHP 8.0 entfernt. Stattdessen soll NumberFormatter::formatCurrency() verwendet werden.

Diese Klasse und Methode gehört zu den intl-Funktionen. Macht es dann nicht Sinn, komplett auf die intl-Funktionen umzustellen und die locale-Abhängigkeiten aufzulösen? Sind alle erforderlichen Funktionen verfügbar und kompatibel? Und auch von der Geschwindigkeit her zumindest nicht deutlich schlechter?

Die int-Funktionen sind sehr umfangreich und bieten viel viel mehr, beispielsweise Kalender-Funktionen. Sie sind aber zum Teil recht schlecht dokumentiert, wie z.B. der Transliterator.

Zur Umschaltung der locales muss und kann nicht der Aufruf von setlocale() verwendet werden, die intl-Funktionen benutzen "Formatter" bzw. Formatierer für die unterschiedlichen Umgebungen/Sprachen. Daher wird für die intl-Testfälle ein neues Interface eingeführt, das lediglich die Formatierung eines Testparameters enthält, siehe FormatterInterface.php

intl currency

Formatierung von Zahlen zur Währungsausgabe mittels NumberFormatter::formatCurrency().

Es wird wie zuvor die Fließkommazahl 20210901.1050 zur Währungsausgabe benutzt. Als Formatierer wird einfach der NumberFormatter::CURRENCY benutzt. Die Initialisierung des Formatters wird NICHT in die Zeitmessung einbezogen, denn das wäre von der Programmierung her unsinnig und praxisfern.

DataFormatterCurrency.php
<?php
...
        $this->formatter = new NumberFormatter($localeName, NumberFormatter::CURRENCY);
...
        return $this->formatter->format($sample);
...

intl decimal

Formatierung von Zahlen, bezüglich Tausender-Trennzeichen und Kommazeichen mittels NumberFormatter::format().

Wie oben, jedoch unter Verwendung des NumberFormatter::DECIMAL.

DataFormatterDecimal.php
<?php
...
        $this->formatter = new NumberFormatter($localeName, NumberFormatter::DECIMAL);
...
        return $this->formatter->format($sample);
...

intl datetime

Formatierung von Datumsangaben mittels IntlDateFormatter::format().

Ebenfalls wie oben, jedoch unter Verwendung des IntlDateFormatter. Als Ausgabe wird das "lange" Datumsformat, sowie das "kurze" Zeitformat gewählt.

DataFormatterDate.php
<?php
...
        $this->formatter = new IntlDateFormatter($localeName, IntlDateFormatter::LONG, IntlDateFormatter::SHORT);
...
        return $this->formatter->format($sample);
...

intl translit

Übersetzung bzw. Formatierung von lokalisierten Zeichenketten mittels Transliterator::transliterate().

Es wird der gleiche String wie oben für "locales" transformiert, jedoch ist die Initialisierung der Translit-Klasse deutlich komplexer. Dazu unten bei den Auswertungen mehr. In diesem Test wird für die "plain" Version die Anweisung Latin-ASCII benutzt, für die "localized" Version die Anweisungen de-ASCII; Any-Latin; Latin-ASCII; [\u0080-\u7fff] remove.

index-localized-formatter-translit.php
<?php
...
        $uut = new DataFormatterTranslit('de-ASCII; Any-Latin; Latin-ASCII; [\u0080-\u7fff] remove');
...
DataFormatterTranslit.php
<?php
...
        $this->translitarator = Transliterator::create($transliteratorIds);
...
        return $this->translitarator->transliterate($sample);
...

Testlauf

Die Tests werden dann wie in dem Readme beschrieben nacheinander gestartet, wobei die Zeiten gemessen werden, siehe auch README.md. Mir erscheint es sinnvol, erst die locale Tests zu fahren, dann die formatter Tests. Jeweils erst in der "plain" und dann in der "localized" Version. Hier die Ergebnisse:

host:~/php-performance-tests/test-3-localesfor TEST in \
  index-plain-translit.php \
  index-plain-datetime.php \
  index-plain-decimal.php \
  index-plain-currency.php \
  index-localized-translit.php \
  index-localized-datetime.php \
  index-localized-decimal.php \
  index-localized-currency.php \
  index-plain-formatter-translit.php \
  index-plain-formatter-datetime.php \
  index-plain-formatter-decimal.php \
  index-plain-formatter-currency.php \
  index-localized-formatter-translit.php \
  index-localized-formatter-datetime.php \
  index-localized-formatter-decimal.php \
  index-localized-formatter-currency.php \
  ; do

  echo "${TEST}"
  for i in 1000 2000 4000 8000 16000 32000 64000 128000 256000 \
    512000 1024000 2048000 4096000 8192000 16348000 32768000 65536000 \
  ; do php "${TEST}" "${i}" ; done
done
index-plain-translit.php
1000 (??? ??? ss EUR): 0.001 s
2000 (??? ??? ss EUR): 0.002 s
4000 (??? ??? ss EUR): 0.003 s
8000 (??? ??? ss EUR): 0.007 s
16000 (??? ??? ss EUR): 0.013 s
32000 (??? ??? ss EUR): 0.026 s
64000 (??? ??? ss EUR): 0.053 s
128000 (??? ??? ss EUR): 0.105 s
256000 (??? ??? ss EUR): 0.210 s
512000 (??? ??? ss EUR): 0.429 s
1024000 (??? ??? ss EUR): 0.858 s
2048000 (??? ??? ss EUR): 1.676 s
4096000 (??? ??? ss EUR): 3.347 s
8192000 (??? ??? ss EUR): 6.732 s
16348000 (??? ??? ss EUR): 13.585 s
32768000 (??? ??? ss EUR): 27.063 s
65536000 (??? ??? ss EUR): 54.541 s

index-plain-datetime.php
1000 (Sunday 10/31/21): 0.001 s
2000 (Sunday 10/31/21): 0.003 s
4000 (Sunday 10/31/21): 0.006 s
8000 (Sunday 10/31/21): 0.011 s
16000 (Sunday 10/31/21): 0.025 s
32000 (Sunday 10/31/21): 0.047 s
64000 (Sunday 10/31/21): 0.091 s
128000 (Sunday 10/31/21): 0.181 s
256000 (Sunday 10/31/21): 0.362 s
512000 (Sunday 10/31/21): 0.722 s
1024000 (Sunday 10/31/21): 1.444 s
2048000 (Sunday 10/31/21): 2.886 s
4096000 (Sunday 10/31/21): 5.773 s
8192000 (Sunday 10/31/21): 11.559 s
16348000 (Sunday 10/31/21): 23.031 s
32768000 (Sunday 10/31/21): 46.172 s
65536000 (Sunday 10/31/21): 92.337 s

index-plain-decimal.php
1000 (20210901.11): 0.000 s
2000 (20210901.11): 0.001 s
4000 (20210901.11): 0.001 s
8000 (20210901.11): 0.003 s
16000 (20210901.11): 0.006 s
32000 (20210901.11): 0.011 s
64000 (20210901.11): 0.022 s
128000 (20210901.11): 0.047 s
256000 (20210901.11): 0.087 s
512000 (20210901.11): 0.174 s
1024000 (20210901.11): 0.347 s
2048000 (20210901.11): 0.690 s
4096000 (20210901.11): 1.383 s
8192000 (20210901.11): 2.814 s
16348000 (20210901.11): 5.559 s
32768000 (20210901.11): 11.053 s
65536000 (20210901.11): 22.497 s

index-plain-currency.php
1000 (20210901.11): 0.001 s
2000 (20210901.11): 0.001 s
4000 (20210901.11): 0.002 s
8000 (20210901.11): 0.005 s
16000 (20210901.11): 0.010 s
32000 (20210901.11): 0.019 s
64000 (20210901.11): 0.038 s
128000 (20210901.11): 0.078 s
256000 (20210901.11): 0.152 s
512000 (20210901.11): 0.296 s
1024000 (20210901.11): 0.609 s
2048000 (20210901.11): 1.227 s
4096000 (20210901.11): 2.493 s
8192000 (20210901.11): 4.903 s
16348000 (20210901.11): 9.534 s
32768000 (20210901.11): 19.552 s
65536000 (20210901.11): 38.279 s

index-localized-translit.php
1000 (aeoeue AEOEUE ss EUR): 0.002 s
2000 (aeoeue AEOEUE ss EUR): 0.004 s
4000 (aeoeue AEOEUE ss EUR): 0.008 s
8000 (aeoeue AEOEUE ss EUR): 0.016 s
16000 (aeoeue AEOEUE ss EUR): 0.031 s
32000 (aeoeue AEOEUE ss EUR): 0.062 s
64000 (aeoeue AEOEUE ss EUR): 0.122 s
128000 (aeoeue AEOEUE ss EUR): 0.243 s
256000 (aeoeue AEOEUE ss EUR): 0.507 s
512000 (aeoeue AEOEUE ss EUR): 0.993 s
1024000 (aeoeue AEOEUE ss EUR): 2.048 s
2048000 (aeoeue AEOEUE ss EUR): 3.915 s
4096000 (aeoeue AEOEUE ss EUR): 7.888 s
8192000 (aeoeue AEOEUE ss EUR): 15.593 s
16348000 (aeoeue AEOEUE ss EUR): 31.518 s
32768000 (aeoeue AEOEUE ss EUR): 62.302 s
65536000 (aeoeue AEOEUE ss EUR): 126.470 s

index-localized-datetime.php
1000 (Sonntag 31.10.2021): 0.002 s
2000 (Sonntag 31.10.2021): 0.005 s
4000 (Sonntag 31.10.2021): 0.010 s
8000 (Sonntag 31.10.2021): 0.023 s
16000 (Sonntag 31.10.2021): 0.040 s
32000 (Sonntag 31.10.2021): 0.083 s
64000 (Sonntag 31.10.2021): 0.161 s
128000 (Sonntag 31.10.2021): 0.324 s
256000 (Sonntag 31.10.2021): 0.635 s
512000 (Sonntag 31.10.2021): 1.235 s
1024000 (Sonntag 31.10.2021): 2.483 s
2048000 (Sonntag 31.10.2021): 5.030 s
4096000 (Sonntag 31.10.2021): 10.222 s
8192000 (Sonntag 31.10.2021): 19.788 s
16348000 (Sonntag 31.10.2021): 39.444 s
32768000 (Sonntag 31.10.2021): 79.135 s
65536000 (Sonntag 31.10.2021): 159.939 s

index-localized-decimal.php
1000 (20.210.901,11): 0.001 s
2000 (20.210.901,11): 0.003 s
4000 (20.210.901,11): 0.005 s
8000 (20.210.901,11): 0.010 s
16000 (20.210.901,11): 0.021 s
32000 (20.210.901,11): 0.042 s
64000 (20.210.901,11): 0.086 s
128000 (20.210.901,11): 0.165 s
256000 (20.210.901,11): 0.337 s
512000 (20.210.901,11): 0.664 s
1024000 (20.210.901,11): 1.332 s
2048000 (20.210.901,11): 2.799 s
4096000 (20.210.901,11): 5.360 s
8192000 (20.210.901,11): 11.180 s
16348000 (20.210.901,11): 21.499 s
32768000 (20.210.901,11): 44.419 s
65536000 (20.210.901,11): 90.431 s

index-localized-currency.php
1000 (20.210.901,11 EUR): 0.002 s
2000 (20.210.901,11 EUR): 0.003 s
4000 (20.210.901,11 EUR): 0.007 s
8000 (20.210.901,11 EUR): 0.013 s
16000 (20.210.901,11 EUR): 0.028 s
32000 (20.210.901,11 EUR): 0.053 s
64000 (20.210.901,11 EUR): 0.109 s
128000 (20.210.901,11 EUR): 0.215 s
256000 (20.210.901,11 EUR): 0.423 s
512000 (20.210.901,11 EUR): 0.873 s
1024000 (20.210.901,11 EUR): 1.726 s
2048000 (20.210.901,11 EUR): 3.487 s
4096000 (20.210.901,11 EUR): 7.192 s
8192000 (20.210.901,11 EUR): 13.809 s
16348000 (20.210.901,11 EUR): 27.475 s
32768000 (20.210.901,11 EUR): 54.766 s
65536000 (20.210.901,11 EUR): 110.048 s

index-plain-formatter-translit.php
1000 (aou AOU ss €): 0.005 s
2000 (aou AOU ss €): 0.010 s
4000 (aou AOU ss €): 0.020 s
8000 (aou AOU ss €): 0.045 s
16000 (aou AOU ss €): 0.081 s
32000 (aou AOU ss €): 0.160 s
64000 (aou AOU ss €): 0.322 s
128000 (aou AOU ss €): 0.643 s
256000 (aou AOU ss €): 1.270 s
512000 (aou AOU ss €): 2.549 s
1024000 (aou AOU ss €): 5.072 s
2048000 (aou AOU ss €): 10.163 s
4096000 (aou AOU ss €): 20.729 s
8192000 (aou AOU ss €): 40.777 s
16348000 (aou AOU ss €): 81.041 s
32768000 (aou AOU ss €): 163.959 s
65536000 (aou AOU ss €): 328.257 s

index-plain-formatter-datetime.php
1000 (31. Oktober 2021 um 13:46): 0.004 s
2000 (31. Oktober 2021 um 13:46): 0.009 s
4000 (31. Oktober 2021 um 13:46): 0.017 s
8000 (31. Oktober 2021 um 13:46): 0.034 s
16000 (31. Oktober 2021 um 13:46): 0.068 s
32000 (31. Oktober 2021 um 13:46): 0.133 s
64000 (31. Oktober 2021 um 13:46): 0.272 s
128000 (31. Oktober 2021 um 13:46): 0.544 s
256000 (31. Oktober 2021 um 13:46): 1.085 s
512000 (31. Oktober 2021 um 13:46): 2.117 s
1024000 (31. Oktober 2021 um 13:46): 4.382 s
2048000 (31. Oktober 2021 um 13:46): 8.748 s
4096000 (31. Oktober 2021 um 13:46): 17.175 s
8192000 (31. Oktober 2021 um 13:46): 33.927 s
16348000 (31. Oktober 2021 um 13:47): 68.888 s
32768000 (31. Oktober 2021 um 13:48): 137.006 s
65536000 (31. Oktober 2021 um 13:50): 275.256 s

index-plain-formatter-decimal.php
1000 (20.210.901,105): 0.001 s
2000 (20.210.901,105): 0.002 s
4000 (20.210.901,105): 0.003 s
8000 (20.210.901,105): 0.006 s
16000 (20.210.901,105): 0.012 s
32000 (20.210.901,105): 0.024 s
64000 (20.210.901,105): 0.048 s
128000 (20.210.901,105): 0.095 s
256000 (20.210.901,105): 0.188 s
512000 (20.210.901,105): 0.384 s
1024000 (20.210.901,105): 0.783 s
2048000 (20.210.901,105): 1.556 s
4096000 (20.210.901,105): 2.931 s
8192000 (20.210.901,105): 6.410 s
16348000 (20.210.901,105): 12.214 s
32768000 (20.210.901,105): 24.199 s
65536000 (20.210.901,105): 47.922 s

index-plain-formatter-currency.php
1000 (20.210.901,10 XXX): 0.001 s
2000 (20.210.901,10 XXX): 0.002 s
4000 (20.210.901,10 XXX): 0.004 s
8000 (20.210.901,10 XXX): 0.007 s
16000 (20.210.901,10 XXX): 0.013 s
32000 (20.210.901,10 XXX): 0.030 s
64000 (20.210.901,10 XXX): 0.057 s
128000 (20.210.901,10 XXX): 0.109 s
256000 (20.210.901,10 XXX): 0.227 s
512000 (20.210.901,10 XXX): 0.442 s
1024000 (20.210.901,10 XXX): 0.878 s
2048000 (20.210.901,10 XXX): 1.733 s
4096000 (20.210.901,10 XXX): 3.493 s
8192000 (20.210.901,10 XXX): 6.969 s
16348000 (20.210.901,10 XXX): 13.873 s
32768000 (20.210.901,10 XXX): 27.565 s
65536000 (20.210.901,10 XXX): 54.863 s

index-localized-formatter-translit.php
1000 (aeoeue AEOEUE ss ): 0.025 s
2000 (aeoeue AEOEUE ss ): 0.050 s
4000 (aeoeue AEOEUE ss ): 0.101 s
8000 (aeoeue AEOEUE ss ): 0.200 s
16000 (aeoeue AEOEUE ss ): 0.410 s
32000 (aeoeue AEOEUE ss ): 0.812 s
64000 (aeoeue AEOEUE ss ): 1.606 s
128000 (aeoeue AEOEUE ss ): 3.228 s
256000 (aeoeue AEOEUE ss ): 6.453 s
512000 (aeoeue AEOEUE ss ): 13.148 s
1024000 (aeoeue AEOEUE ss ): 25.422 s
2048000 (aeoeue AEOEUE ss ): 52.548 s
4096000 (aeoeue AEOEUE ss ): 102.689 s
8192000 (aeoeue AEOEUE ss ): 207.471 s
16348000 (aeoeue AEOEUE ss ): 415.153 s
32768000 (aeoeue AEOEUE ss ): 827.463 s
65536000 (aeoeue AEOEUE ss ): 1683.279 s

index-localized-formatter-datetime.php
1000 (31. Oktober 2021 um 16:29): 0.005 s
2000 (31. Oktober 2021 um 16:29): 0.009 s
4000 (31. Oktober 2021 um 16:29): 0.018 s
8000 (31. Oktober 2021 um 16:29): 0.034 s
16000 (31. Oktober 2021 um 16:29): 0.069 s
32000 (31. Oktober 2021 um 16:29): 0.136 s
64000 (31. Oktober 2021 um 16:29): 0.275 s
128000 (31. Oktober 2021 um 16:29): 0.550 s
256000 (31. Oktober 2021 um 16:29): 1.105 s
512000 (31. Oktober 2021 um 16:29): 2.220 s
1024000 (31. Oktober 2021 um 16:29): 4.310 s
2048000 (31. Oktober 2021 um 16:29): 8.628 s
4096000 (31. Oktober 2021 um 16:29): 17.251 s
8192000 (31. Oktober 2021 um 16:30): 34.832 s
16348000 (31. Oktober 2021 um 16:30): 68.699 s
32768000 (31. Oktober 2021 um 16:31): 138.835 s
65536000 (31. Oktober 2021 um 16:34): 276.134 s

index-localized-formatter-decimal.php
1000 (20.210.901,105): 0.001 s
2000 (20.210.901,105): 0.002 s
4000 (20.210.901,105): 0.003 s
8000 (20.210.901,105): 0.006 s
16000 (20.210.901,105): 0.012 s
32000 (20.210.901,105): 0.024 s
64000 (20.210.901,105): 0.048 s
128000 (20.210.901,105): 0.102 s
256000 (20.210.901,105): 0.191 s
512000 (20.210.901,105): 0.394 s
1024000 (20.210.901,105): 0.798 s
2048000 (20.210.901,105): 1.598 s
4096000 (20.210.901,105): 3.019 s
8192000 (20.210.901,105): 6.107 s
16348000 (20.210.901,105): 12.208 s
32768000 (20.210.901,105): 24.485 s
65536000 (20.210.901,105): 49.003 s

index-localized-formatter-currency.php
1000 (20.210.901,10 €): 0.001 s
2000 (20.210.901,10 €): 0.002 s
4000 (20.210.901,10 €): 0.004 s
8000 (20.210.901,10 €): 0.007 s
16000 (20.210.901,10 €): 0.014 s
32000 (20.210.901,10 €): 0.029 s
64000 (20.210.901,10 €): 0.055 s
128000 (20.210.901,10 €): 0.110 s
256000 (20.210.901,10 €): 0.232 s
512000 (20.210.901,10 €): 0.466 s
1024000 (20.210.901,10 €): 0.928 s
2048000 (20.210.901,10 €): 1.814 s
4096000 (20.210.901,10 €): 3.661 s
8192000 (20.210.901,10 €): 6.916 s
16348000 (20.210.901,10 €): 14.146 s
32768000 (20.210.901,10 €): 27.686 s
65536000 (20.210.901,10 €): 56.122 s

Die einzelnen Werte in einer Tabelle aufgelistet sind im sourcecode auf gitlab.com zu finden. Die Zeitmessung in Diagrammen dargestellt:

localetest-1

Oder noch einmal anders dargestellt als Geschwindigkeit Sekunden pro Iteration (mit logarithmischer Y-Aches):

localetest-2

Die Geschwindigkeit bleibt einigermaßen konstant auch bei vielen Iterationen, daher hier die durchschnittlichen Geschwindigkeiten, ebenfalls logarithmisch dargestellt. Das Diagramm zeigt die Zeit für 1.000.000 Iterationen, ein hoher Wert bedeutet also eine schlechtere Geschwindigkeit.

localetest-3

Auswertungen

1.: Performance Transliteration

Sehr auffällig ist, dass die Geschwindigkeit der Transliteration mit den ICU-Komponenten sehr viel Performance frisst. Die logarithmische Achsenformatierung macht das nicht so offensichtlich: die Zeit für eine ICU-Transliteration ist mehr als 10-mal so hoch wie die für eine Transliteration mit iconv/locale!

Dazu muss man aber auch sagen, dass die ICU-Transliteration sehr sehr viel mehr kann, als iconv. Zeichenketten wie キャンパス oder Αλφαβητικός Κατάλογος oder биологическом werden problemlos umformatiert, das kann iconv nicht. Die benutzten Anweisungen de-ASCII; Any-Latin; Latin-ASCII; [\u0080-\u7fff] remove bedeuten:

Gerade die Regel alles nach Latin zu konvertieren, ist sehr aufwendig, bewirkt aber wie gesagt, dass die Zeichen aus allen möglichen Sprachen soweit möglich umkonvertiert werden. Wenn man das will oder braucht, ist es das definitiv Wert!

Auf unicode.org ist eine Demo-Seite erreichbar, mit der sich unterschiedliche Transformationen testen lassen: https://icu4c-demos.unicode.org/icu-bin/translit. Außerdem liegt in dem sourcecode auf gitlab.com ein weiteres Test-Script, mit dem man unterschiedliche Zeichen und Translit-Regeln ausprobieren kann:

host:~/php-performance-tests/test-3-localesphp test-icu-translit.php
iterations: 0
sample: äöü ÄÖÜ ß €
ruleset: de-ASCII;Any-Latin;Latin-ASCII
result: aeoeue AEOEUE ss €
time: 0.000 s
host:~/php-performance-tests/test-3-localesphp test-icu-translit.php 102400
iterations: 102400
sample: äöü ÄÖÜ ß €
ruleset: de-ASCII;Any-Latin;Latin-ASCII
result: aeoeue AEOEUE ss €
time: 2.497 s
host:~/php-performance-tests/test-3-localesphp test-icu-translit.php 102400 "äöü ÄÖÜ ß €" "Latin-ASCII"
iterations: 102400
sample: äöü ÄÖÜ ß €
ruleset: Latin-ASCII
result: aou AOU ss €
time: 0.500 s
host:~/php-performance-tests/test-3-localesphp test-icu-translit.php 102400 "äöü ÄÖÜ ß €" "de-ASCII;Latin-ASCII"
iterations: 102400
sample: äöü ÄÖÜ ß €
ruleset: de-ASCII;Latin-ASCII
result: aeoeue AEOEUE ss €
time: 2.518 s

Das Wort Any in z.B. Any-Latin steht für alle Zeichen, es werden also alle Regeln angewandt, siehe Transliterator Identifiers und ICU Transliterators Dokumentation. Der Begriff Latin ist weiter gefasst, als die klassischen Latin-Zeichensätze nach ISO-8859, siehe Script/Language Referenz.

Der dritte und vierte Test zeigen, wie aufwändig die Regel de-ASCII tatsächlich ist. Woran liegt das? Die beiden relevanten Regelsätze sind dank open source auf github einsehbar:

de-ASCII: https://github.com/unicode-org/cldr/blob/main/common/transforms/de-ASCII.xml

Latin-ASCII: https://github.com/unicode-org/cldr/blob/main/common/transforms/Latin-ASCII.xml

In den Regeln für de-ASCII wird auf die Transformations-Regel für Any-ASCII verwiesen und ich vermute, dass diese geerbt oder irgendwie anders zusätzlich Anwendung findet. Wie das genau zusammenhängt, kann ich jedoch nicht sagen, das würde auch den Rahmen dieses Artikels sprengen.

2.: Performance Andere

Etwas unerwartet ist auch die Messung der Zeit bei den Datums-Formatierungen. Der Aufwand ist sowohl mit den locale-abhängigen Funktionen, als auch mit den intl-Funktionen erstaunlich hoch. Jedoch lange nicht so hoch wie bei der Transliteration, und auch der Unterschied zwischen den Zeiten ist lange nicht so deutlich.

Die Zeiten für Formatierung von Dezimalzahlen und Währungen ist jedoch mit beiden Komponenten annähernd gleich. Für die Ablösung der PHP-Funktion money_format() (die ja wie schon erwähnt als DEPRECATED markiert und in PHP Version 8.0 nicht mehr verfügbar ist) kann man beim Umstieg auf die intl-Funktion sogar mit einem leichten Performance-Gewinn rechnen.

3.: Umfang/Kompatibilität

Wie schon erwähnt sind die Möglichkeiten bei der Transliteration mit den ICU-Komponenten sehr viel umfangreicher. Während bei der Transliteration mit iconv das Euro-Zeichen in den String "EUR" konvertiert wurde, bleibt es bei Transliteration mit ICU/CLDR als Euro-Symbol erhalten. Und das finde ich auch richtig, denn die Kovertierung von Währungen ist ein anderes Thema. Bei den ICU-Komponenten funktoniert es jedoch andersherum: Das Währungssymbol wird aus dem Währungsnamen/-kürzel gebildet, siehe NumberFormatter::formatCurrency().

Die Transformation des Währungs-Symbols nach EUR fällt mit dem Umstieg auf die intl-Funktionen weg.

Hier noch ein paar Testläufe:

host:~/php-performance-tests/test-3-localesphp test-icu-translit.php 102400 "@€ ?¿¡ äßæⱠ ©® ㎉ ㎧ ¼ m² „” キャンパス 高纳" "Latin-ASCII"
iterations: 102400
sample: @€ ?¿¡ äßæⱠ ©® ㎉ ㎧ ¼ m² „” キャンパス 高纳
ruleset: Latin-ASCII
result: @€ ?¿¡ assaeL (C)(R) kcal m/s  1/4 m² ,," キャンパス 高纳
time: 1.706 s
host:~/php-performance-tests/test-3-localesphp test-icu-translit.php 102400 "@€ ?¿¡ äßæⱠ ©® ㎉ ㎧ ¼ m² „” キャンパス 高纳" "Any-Latin;Latin-ASCII"
iterations: 102400
sample: @€ ?¿¡ äßæⱠ ©® ㎉ ㎧ ¼ m² „” キャンパス 高纳
ruleset: Any-Latin;Latin-ASCII
result: @€ ?¿¡ assaeL (C)(R) kcal m/s  1/4 m² ,," kyanpasu gao na
time: 6.476 s
host:~/php-performance-tests/test-3-localesphp test-icu-translit.php 102400 "a á à ą b b́ c ć ç d θ θ´ θ˙ é è ę f g h ch i j k l ł m ḿ n ń o ó p ṕ q r ŗ ſ σ ß t v w ẃ x y z ź ƶ"
iterations: 102400
sample: a á à ą b b́ c ć ç d θ θ´ θ˙ é è ę f g h ch i j k l ł m ḿ n ń o ó p ṕ q r ŗ ſ σ ß t v w ẃ x y z ź ƶ
ruleset: de-ASCII;Any-Latin;Latin-ASCII
result: a a a a b b c c c d th th´ th˙ e e e f g h ch i j k l l m m n n o o p p q r r s s ss t v w w x y z z z
time: 12.563 s
host:~/php-performance-tests/test-3-localesphp
<?php
var_dump(iconv('UTF-8','ASCII//TRANSLIT', 'a á à ą b b́ c ć ç d θ θ´ θ˙ é è ę f g h ch i j k l ł m ḿ n ń o ó p ṕ q r ŗ ſ σ ß t v w ẃ x y z ź ƶ'));
string(99) "a a a a b b c c c d ? ?' ?? e e e f g h ch i j k l l m m n n o o p p q r r s ? ss t v w w x y z z z"

Der erste Test zeigt, dass meine Version der libicu nicht ganz aktuell ist, denn das umgedrehte Frage- und Ausrufezeichen sollte bei Latin-ASCII in den aktuellen Versionen ebenfalls transformiert werden.

Die letzten beiden Tests zeigen, dass die Transliteration mittels intl-Funktionen andere Ergebnisse bringen kann, als bei iconv(), hier am Beispiel des polnischen Alphabets.

Ich habe eingangs geschrieben, dass ich vermute, dass in anderen Sprachen die Sonderzeichen wie ä bei der Transliteration nicht in mehrbuchstabige Äquivalente transformiert werden. Das ist falsch. Gerade an dem Beispiel des polnischen Alphabets oben ist das deutlich ersichtlich. Grund der Annahme war ursprünglich, dass ich keine andere Konvertierungsregel in der Art wie de-ASCII in den ICU-Komponenten finden konnte. Die Liste der Regelsätze gefiltert nach ASCII ergibt bei mir lokal:

host:~/php-performance-tests/test-3-localesphp | grep ASCII
<?php
var_dump(Transliterator::listIDs());
  string(11) "ASCII-Latin"
  string(11) "Latin-ASCII"
  string(8) "de-ASCII"

Die deutschen Sonderzeichen "äöü" sind in der Latin-ASCII Regel nicht vorhanden, obwohl sehr viele andere Sonderzeichen und auch Punktuationen enthalten sind. Ein anderes deutsches Sonderzeichen, das ß ist jedoch enthalten, das Zeichen wird also "universell" in der Art transformiert, siehe wieder das polnische Alphabet oben. Warum äöü nicht ebenfalls enthalten sind, erschließt sich mir nicht. Offenbar liegt die Transformation zu aou im internationalen Kontext näher, so dass diese Ausnahmeregel (CLDR Version 32 bzw. ICU Version 60) eingeführt wurde.

Zusammenfassung

Schickt Kommentare oder Anmerkungen bitte über Twitter an @limitland oder @jlue.

Referenzen

Unicode projects homepage: https://home.unicode.org/basic-info/projects/
ICU Dokumentation Script Transliteration: https://unicode-org.github.io/icu/userguide/transforms/general/#script-transliteration
ICU Dokumentation Ternsliterators: https://unicode-org.github.io/icu/userguide/transforms/general/#icu-transliterators
Unicode CLDR Project: https://cldr.unicode.org/
CLDR Transform sources: https://github.com/unicode-org/cldr/tree/main/common/transforms
CLDR Transform de-ASCII: https://github.com/unicode-org/cldr/blob/main/common/transforms/de-ASCII.xml
CLDR Transform Latin-ASCII: https://github.com/unicode-org/cldr/blob/main/common/transforms/Latin-ASCII.xml
ICU Transform Demonstration: https://icu4c-demos.unicode.org/icu-bin/translit
Unicode 14.0 Character Code Charts: https://www.unicode.org/charts/index.html