Performance des UUIDs
Tags: php, symfony, programming, performance
Published: 2020-03-20 16:54:45

---
title: Performance des UUIDs
published: true
description: Comparaison sur le temps de conversion des UUIDs, du format binaire au format chaîne, et optimisation possible.
tags: #php #symfony #programming #performance
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/mjw4wnfy7pizn19ls72b.jpg
---
([English version](https://dev.to/fredbouchery/performance-of-uuids-3i40))

Dernièrement, en analysant les performances d'une application au moyen de l'excellent outil [blackfire](https://blackfire.io/), nous avons constaté un temps non-négligeable passé à transformer des UUIDs binaires en chaînes et vice-versa.

Il faut dire que le stockage des UUIDs dans la base de données est au format binaire, et l'application disposant d'une API Rest, les UUIDS sont donc tout le temps convertis en chaîne de caractères sous la forme `"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx"`, que ce soit pour retourner la réponse en JSON ou pour traiter des requêtes avec des UUIDs.

Comme beaucoup, les développeurs de l'application se sont tournés vers le paquet "[Ramsey/UUID](https://github.com/ramsey/uuid)", qui semble être la solution majoritairement exploitée dans le cadre de la manipulation d'UUIDs. L'excellente réputation de ce composant, nous a conduit à chercher des solutions d'optimisation dans l'application, sans remettre en doute la pertinence de Ramsey/UUID.

Récemment, à la lecture de l'article de Jolicode "[UUID generation in PHP](https://jolicode.com/blog/uuid-generation-in-php)" et du débat autour d'une pull request sur [Symfony UID](https://twitter.com/matthieunapoli/status/1238541015381835778), nous nous sommes penchés un peu plus sur la façon dont l'application manipule les UUIDs, car comme le dit [Grégoire Pineau](https://jolicode.com/equipe/gregoire-pineau), si le polyfill de Symfony est aussi performant, il faudrait peut-être remettre en cause l'utilisation de Ramsey/UUID.

Seulement, quand on fait quelques statistiques sur l'application, on passe surtout beaucoup de temps à coder/décoder les UUIDs plutôt qu'à en générer, et ça doit être le cas de beaucoup d'applications.

Mais dans ce cas, le benchmark de Jolicode est-il toujours pertinent, dans la mesure où celui-ci fait une comparaison sur la génération d'UUID ?

Nous avons donc fait d'autres benchmarks, mais cette fois-ci, en comparant une opération qui converti un UUID binaire en chaîne et une chaîne en binaire.

Avec Ramsey/UUID, cela est fait quand on crée un objet Uuid depuis un binaire, puis que l'on retransforme cet objet en binaire : 
```php
<?php
\Ramsey\Uuid\Uuid::fromBytes($bytes)->getBytes();
```

Nous avons créé une classe `Uuid` qui utilise l'extension PHP "[uuid](https://pecl.php.net/package/uuid)", sous la forme d'un [ValueObject](https://en.wikipedia.org/wiki/Value_object):
```php
<?php
final class Uuid {
    /**
     * @var string
     */
    private $uuid;

    public function __construct(string $uuid)
    {
        if (\uuid_is_valid($uuid) === false) {
            throw new RuntimeException("Wrong UUID format");
        }
        $this->uuid = $uuid;
    }

    public function equals(Uuid $other): bool
    {
        return $this->uuid === $other->uuid;
    }

    public static function fromBytes(string $bytes): self
    {
        return new self(\uuid_unparse($bytes));
    }

    public function getBytes(): string
    {
        return \uuid_parse($this->uuid);
    }

    public function toString(): string
    {
        return $this->uuid;
    }
}
```

Avant même de benchmarker un `Uuid::fromBytes($bytes)->getBytes()`, on sait déjà que ça sera plus performant que Ramsey car il n'y a pas d'appel sur des factories et autres codecs. De plus, en interne, Ramsey conserve l'UUID sous une forme décomposée en plusieurs champs, alors que dans notre code précédent, l'UUID est mémorisé sous sa forme chaîne de caractères.

Nous avons du coup regardé comment était réalisé le [polyfill Symfony](https://github.com/symfony/polyfill-uuid) qui permet de remplacer les fonctions de l'extension UUID quand elles n'existent pas, et nous nous sommes dit qu'il était possible de l'optimiser, car un traitement un peu gourmand était fait lors du uuid_unparse pour valider la structure de l'UUID.

Voici comment nous pourrions réaliser cette optimisation : 

```php
<?php
final class Uuid {
    /**
     * @var string
     */
    private $uuid;

    public function __construct(string $uuid)
    {
        if (\preg_match('`^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$`Di', $uuid) === 0) {
            throw new RuntimeException("Wrong UUID format");
        }
        $this->uuid = $uuid;
    }

    public function equals(Uuid $other): bool
    {
        return $this->uuid === $other->uuid;
    }

    public static function fromBytes(string $bytes): self
    {
        if (\strlen($bytes) !== 16) {
            throw new RuntimeException("Invalid binary UUID. Length is not 16 bytes");
        }

        $hex = \substr_replace(\bin2hex($bytes), '-', 8, 0);
        $hex = \substr_replace($hex, '-', 13, 0);
        $hex = \substr_replace($hex, '-', 18, 0);
        return new self(\substr_replace($hex, '-', 23, 0));
    }

    public function getBytes(): string
    {
        return \hex2bin(\str_replace('-', '', $this->uuid));
    }

    public function toString(): string
    {
        return $this->uuid;
    }
}
```

Le temps d’exécution pour 1 million d'itérations nous donne :
```
Ramsey => 2583 ms
ext-uuid => 1260 ms
polyfill-symfony => 4020 ms
custom => 642 ms
```
*(l'environnement n'a pas beaucoup d'importance, sachez juste que le test a été fait sur Ubuntu 19.10 avec PHP 7.3.11)*

![What ?](https://dev-to-uploads.s3.amazonaws.com/i/q0urzzy1j0209lok217g.jpg)

Nous avons ré-exécuté ce benchmark plusieurs fois pour en être sûr, mais oui, le code est deux fois plus rapide que l'extension UUID !

Quant à la génération des UUIDs, comme c'est une opération peu courante pour nous, nous avons continué à utiliser Ramsey pour cela.

Dans notre contexte, on s'aperçoit que Ramsey/UUID n'est pas forcement pertinent, mais ce qui est le plus surprenant, c'est que notre code PHP est plus performant dans l'encodage/décodage d'UUID que l'extension.
Donc, si vous n'avez pas cette extension, ce n'est pas bien grave, car elle est moins performante que sa version en PHP.

Update: Depuis l'écriture de cet article, [Nicolas Grekas](https://twitter.com/nicolasgrekas) a soumis une [PR](https://github.com/symfony/polyfill/pull/244) pour optimiser le polyfill de Symfony.
Voici le benchmark avec le résultat de sa PR:
```
Ramsey => 2583 ms
ext-uuid => 1260 ms
polyfill-symfony => 4020 ms
polyfill-symfony-optim => 1145 ms
custom => 642 ms
```

-----
Voici le code utilisé pour le benchmark (+ un test complémentaire de l'impact sur le check à la construction)

[https://gist.github.com/f2r/4f21279732cc1ba81dddc05ef042a1f5](https://gist.github.com/f2r/4f21279732cc1ba81dddc05ef042a1f5)