Przejdź do treści

Mit o kolekcjach… #2

Dziś na tapet wchodzi temat operacji na kolekcji danych składających się z obiektów oraz tych klasycznych tablicowych. Co jest szybsze, wydajniejsze oraz łatwiejsze w użyciu? Sprawdźmy, jak takie operację wypadają w PHP.

Obiekty kontra tablice…

PHP 8.1 zagościł na salonach… i świetnie, lecz ten wpis nie będzie o tym, co zostało dodane, usunięte, zmienione lub oznaczone jako @deprecated, a o tym, jak sobie radzi z kolekcjami obiektów oraz tablic. Czy iterowanie po takim zbiorze okaże się tak samo szybkie? Co w przypadku edycji poszczególnych elementów zbioru?

Wyższości obiektów nad tablicami raczej nikomu wykładać nie trzeba, ale czy pomimo wygody użytkowania oraz łatwiejszego modelowania naszego docelowego oprogramowania możemy dorzucić jeszcze aspekt wydajnościowy?

Scenariusz testowy jest banalnie prosty. Wymyślamy sobie testowy obiekt z kilkoma polami oraz ekwiwalent w postaci tablicowej. Dla wersji obiektowej oraz tablicowej tworzymy kolekcje o z góry zdefiniowanej wielkości i wykonujemy operacje… to tak z grubsza.

Środowisko testowe oraz scenariusz

Zanim jednak przejdziemy do testowania, kilka słów o sposobie pomiaru, scenariuszu oraz maszynie, na której test zostanie wykonany.
Scenariusz testowy zakłada 3 odrębne testy:

  1. Iterowanie po kolekcji oraz odczyt poszczególnych elementów
  2. Iterowanie po kolekcji oraz modyfikacja aktualnego elementu
  3. Iterowanie po kolekcji oraz kopiowanie aktualnego elementu

Do pomiarów użyto blackfire.io, a maszyna to zwykła developerska stacja robocza działająca pod dyktando systemu operacyjnego Mint 20.3, zwykła intelowa i5-tka, dysk NVMe SN530 oraz circa 16GB pamięci RAM. A na czas testów, uruchomiona była tylko konsola.

#> lscpu
Architektura:                    x86_64
Tryb(y) pracy CPU:               32-bit, 64-bit
Kolejność bajtów:                Little Endian
Address sizes:                   39 bits physical, 48 bits virtual
CPU:                             8
Lista aktywnych CPU:             0-7
Wątków na rdzeń:                 2
Rdzeni na gniazdo:               4
Gniazd:                          1
Węzłów NUMA:                     1
ID producenta:                   GenuineIntel
Rodzina CPU:                     6
Model:                           142
Nazwa modelu:                    Intel(R) Core(TM) i5-10210U CPU @ 1.60GHz
Wersja:                          12
CPU MHz:                         2100.000
CPU max MHz:                     4200,0000
CPU min MHz:                     400,0000

I najważniejsze, bohater dzisiejszego wpisu:

#> php -v
PHP 8.1.1 (cli) (built: Dec 31 2021 07:26:20) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.1.1, Copyright (c) Zend Technologies
    with Zend OPcache v8.1.1, Copyright (c), by Zend Technologies
    with blackfire v1.73.0~linux-x64-non_zts81, https://blackfire.io, by Blackfire

Hola hola, a co ten OPcache tam robi? Spokojnie zadbałem, o to, by nie był używany:

#> cat /etc/php/8.1/cli/conf.d/10-opcache.ini 
; configuration for php opcache module
; priority=10
zend_extension=opcache.so

opcache.enabled=0
opcache.jit=0000

Generujmy wyniki

Zanim przejdziemy dalej, kilka dodatkowych informacji.

Do generowania przykładowych danych zostanie użyta poniższa funkcja:

[php]$generateRandomName = function ($maxElements): string { $subString = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas cursus libero eget diam condimentum, blandit ullamcorper arcu pellentesque. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque faucibus pretium dictum. Nulla mollis eros at nunc consectetur efficitur. ’ . \mt_rand(0, $maxElements + 1); return \str_shuffle($subString); };[/php]

A struktura danych dla obiektu, jak i tablicy, prezentuje się następująco:

[php] // dla obiektu class ExampleObject { public function __construct( public int $id, public string $firstName, public string $lastName, public string $fullName, public string $comment, public int $accessLevel) {} } // dla tablicy [ 'id’ => , 'firstName’ => , 'lastName’ => , 'fullName’ => , 'comment’ => , 'accessLevel’ => ] [/php]

Strzeżonego Pan Bóg strzeże, pomimo, że OPcache jest wyłączony to postanowiłem na końcu każdego testu (po wygenerowaniu wyników) wyczyścić cache, jeśli by się tam pojawił:

[php]clearstatcache(); opcache_reset();[/php]

OK, pokazałem, jak wyglądają przykładowe dane, jak je będę generował, na czym będę uruchamiał, to pozostało mi tylko pokazać kod testowy oraz przekazać kilka informacji na temat wykonania tych testów wraz z interpretacją wyników. Wybrałem następujące wielkości kolekcji testowych: 5000, 10000, 50000, 100000, 200000, 400000 elementów. Każdy test został powtórzony 5-krotnie, a na wykresie zostaną pokazane wartości czasowe dla najszybszego oraz najwolniejszego przejścia testu wraz z średnią arytmetyczną.

Szybki rzut oka na kod źródłowy testów i przeskakujmy do wykresów:

[php] $nonce = ”; $nonceArray = []; // $numberOfElements = aktualna wielkość kolekcji testowej // Test #1 foreach ($arrayToIterate as $item) { $nonce = $item->fullName; // dla wersji obiektowej $nonce = $item[’fullName’]; // dla wersji tablicowej } // Test #2 foreach ($arrayToIterate as $item) { // dla wersji tablicowej używamy referencji &$item $item->id *= $numberOfElements; // dla wersji obiektowej $item[’id’] *= $numberOfElements; // dla wersji tablicowej } // Test #3 foreach ($arrayToIterate as $item) { // dla wersji tablicowej używamy referencji &$item $nonceArray[] = $item; $item->id *= $numberOfElements; // dla wersji obiektowej $item[’id’] *= $numberOfElements; // dla wersji tablicowej } [/php]

Wyniki i wykresiki

Każdy test posiada trzy wykresy, ze względu na ilość elementów oraz skalę. U dołu znajduje się wielkość kolekcji testowej z podziałem na obiekty oraz tablice, a po bokach czas wykonania w jednostce czasu μS. Co za tym idzie im mniej tym lepiej.

Test #1

Test iteruje po kolekcji i odczytuje wartość aktualnego elementu.

Jak widać na wykresach, iterowanie się po kolekcji obiektów jest wydajniejsze niż ta sama operacja na czystych tablicach. Na wykresach pojawiły się jednak dwie dziwne fluktuacje. Pierwsza przy 10k elementów tablicowych, oraz druga przy 50k elementów – gdzie, o dziwo tablice iteruje się szybciej.


Test #2

Iterowanie oraz modyfikacja aktualnego elementu.

Test drugi wykazał te same fluktuacje przy 10k oraz 50k elementów.


Test #3

Iterowanie oraz kopiowanie aktualnego elementu.

Test ostatni wykazał fluktuację tylko przy 10k zbiorze tablic.

Podsumowanie

Właśnie otrzymaliśmy kolejny powód, aby przestać opierać się na tablicach, a zacząć poprawnie modelować nasze aplikacje. Oczywiście pozostaje jeszcze narzut czasowy potrzebny na hydrację danych. Wyniki wykazały zysk nawet ponad 5-cio krotny w niektórych przypadkach, gdzie średnio zyskujemy 1/2 czasu potrzebnego na tę samą operację na tablicach.

Ciekawostką, natomiast okazała się kolekcja 10k tablic, gdzie następuje gwałtowny przeskok czasu przetwarzania takiej tablicy, a to zazwyczaj najpopularniejsza wielkość tablicy przy wszelkiego rodzaju raportach lub eksportach danych.


Image: NASA/Tony Gray/Tom Farrar

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *