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, formatThe 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.
- LC_ALL für alle folgenden Werte
- LC_COLLATE für String-Vergleiche, siehe strcoll()
- LC_CTYPE für Klassifizierung und Umwandlung von Zeichen, zum Beispiel strtoupper()
- LC_MONETARY für localeconv()
- LC_NUMERIC für das Dezimal-Trennzeichen (Siehe auch localeconv())
- LC_TIME für Zeit- und Datums-Formatierungen mittels strftime()
- LC_MESSAGES für Systemmeldungen (verfügbar, wenn PHP mit liblintl kompiliert wurde)
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:
<?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
<?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.
<?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));
<?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.
<?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.
<?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.
<?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.
<?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.
<?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
.
<?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.
<?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
.
<?php ... $uut = new DataFormatterTranslit('de-ASCII; Any-Latin; Latin-ASCII; [\u0080-\u7fff] remove'); ...
<?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:
Oder noch einmal anders dargestellt als Geschwindigkeit Sekunden pro Iteration (mit logarithmischer Y-Aches):
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.
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:
- Formatiere die Sonderzeichen aus den de-nach-ASCII Regeln.
- Dann formatiere alle Zeichen nach "Latin".
- Dann formatiere alle Latin-Zeichen nach ASCII.
- Dann lösche alle Zeichen, die nicht zu ASCII gehören.
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
- Mit dem Wegfall der Funktion money_format in PHP 8.0 wird ein Umstig auf die PHP Komponenten php-intl erforderlich.
- Andere Methoden aus den PHP Internationalisierungs-Komponenten, sowie Text- und Zahlenformatierungen können ebenfalls durch intl-Funktionen ersetzt werden.
- Die intl-Funktionen sind i.d.R. etwas aufwändiger als die herkömmlichen PHP-Funktionen, bieten jedoch auch viel mehr.
- Gerade die Translietration mittels Transliterator ist deutlich aufwendiger, als die mittels iconv(). Jedoch sind die Möglichkeiten auch sehr sehr viel umfangreicher und bieten Unterstützung für sehr viele Sprachen.
- Die Konfiguration des Transliterators ist ebenfalls sehr umfangreich, so dass sich der Aufwand für die Ausführung je nach Bedarf anpassen lässt.
- Die Ergebnisse der Translietration mit Transliterator unterscheiden sich teilweise von der mit iconv().
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