Testowanie wydajności przy tworzeniu zbędnych obiektów w JMH

1

Chciałem przetestować o ile tak na prawdę bardziej wydajne jest unikanie tworzenia dodatkowych obiektów. Temat ten poruszyłem w poście na swoim blogu:
devcave.pl/unikaj-tworzenia-niepotrzebnych-obiektow

Mam tam dwa warianty prostej klasy do sprawdzania czy podana liczba jest poprawną liczbą rzymską:

public class InefficientRomanNumerals {
    static boolean isRomanNumeral(String s) {
        return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"
            + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
    }
}

i

public class RomanNumerals {
    private static final Pattern ROMAN = Pattern.compile(
        "^(?=.)M*(C[MD]|D?C{0,3})"
            + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");

    static boolean isRomanNumeral(String s) {
        return ROMAN.matcher(s).matches();
    }
}

Różnica polega na tym, że w drugim przypadku Pattern jest tworzony i kompilowany tylko raz.

Na początku po prostu sprawdzałem to porównując System.nanoTime(), jednak później zacząłem bawić się z toolem do benchmarków - JMH. O dziwo wyniki bardzo się nie różnią. Tak wygląda benchmark:

@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 5, time = TIME, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 5, time = TIME, timeUnit = TimeUnit.MILLISECONDS)
@Fork(5)
public class PatternBenchmark {

    public static final int LOOPS = 200001;

    @Benchmark
    public void efficientPattern() {
        for (int i = 1; i < LOOPS; i++) {
            RomanNumerals.isRomanNumeral(i + "s");
        }
    }

    @Benchmark
    public void ineficentPattern() {
        for (int i = 1; i < LOOPS; i++) {
            InefficientRomanNumerals.isRomanNumeral(i + "s");
        }
    }
}

i jego wynik:

Benchmark                                                             Mode  Cnt  Score   Error  Units
Avoid_creating_unnecessary_objects.PatternBenchmark.efficientPattern  avgt   25  0.036 ± 0.003   s/op
Avoid_creating_unnecessary_objects.PatternBenchmark.ineficentPattern  avgt   25  0.234 ± 0.011   s/op

~200ms różnicy przy 200 000 obrotów w pętli. Szału nie ma, myślałem, że różnica będzie większa. W przypadku przykładu z autoboxingiem, który też jest w poście, różnica była ogromna - ~1s vs ~7s.

Tylko pytanie - czy ten benchmark jest dobrze napisany? I na ile ten wynik jest miarodajny?

0

Bullshit.

99% przypadków inicjalizacja tych obiektów ma 0 wpływ na wydajność. Dopóki nie zauważysz spadku wydajności jakoś gołym okiem to każde takie [CIACH!] "unikaj zbędnej inicjalizacji" jest warte tyle co nic.

Może jeszcze zaczniemy unikać tworzenia zmiennych poprawiających czytelność żeby zaoszczędzić na wydajność? Please.

Ps: Co do Twojego benchmarku, myślę że jest ok.
Pps: Z tymi 1s/7s też nie jest jakoś tragicznie, bo to ciągle ten sam rząd wielkości.

0

W poście napisałem:

Nie wnioskuj jednak z tego wpisu, że tworzenie obiektów jest kosztowne i powinno być unikane. Wręcz przeciwnie. Tworzenie i zwalnianie z pamięci małych obiektów, których konstruktory nie wykonują dużo pracy nie jest kosztowne. Ponadto, tworzenie dodatkowych obiektów, żeby zwiększyć klarowność i prostotę kodu to zazwyczaj dobra rzecz.

Więc nie wiem skąd taki wniosek ;)

PS: 7 razy wolniej to nie jest jakoś tragicznie? :O

0

~200ms różnicy przy 200 000 obrotów w pętli. Szału nie ma, myślałem, że różnica będzie większa.

234 ms / 36 ms = 6.5 krotna różnica w wydajności. Szału nie ma? Jakie musi być przyspieszenie by warto było się zastanawiać nad lekkim przerobieniem kodu?

ATSD:
GraalVM ma kompilator regeksów do chyba kodu natywnego:

TRegex is an implementation of regular expressions which leverages GraalVM for efficient compilation of automata.

Fajnie byłoby sprawdzić jego wydajność, ale prawdopodobnie wymaga to zbudowania Graala wprost ze źródeł (bo TRegex to świeża sprawa), a to może być wyzwaniem.

2

To są tzw. mikropoptymalizacje. Pozwolę sobie przytoczyć piękny cytat ze stackoverflow (w komentarzu Marcusa Frödina):

unless it's uber-performant code, go with what looks clearest to you and don't spend time on micro-optimization

Trudno znaleźć przykład kodu działającego w praktyce, dla którego Twoja zmiana (unikanie powtórnej inicjalizacji patterna) będzie miała realny wpływ na efekt końcowy. Na przykład na czas, w jakim użytkownik otrzyma przetworzone dane, których zażądał. To jest mniej więcej to, co pisał Piotr, ale może w ten sposób będzie bardziej czytelnie.

Jeżeli natomiast jest prawdopodobne, że Twoja metoda będzie wykonywana mnóstwo razy, to raczej utworzyłbym patterna 1 raz. Intuicyjnie wydaje się to bardziej poprawne niż ciągła inicjalizacja i wielokrotne powtarzanie operacji.

1 użytkowników online, w tym zalogowanych: 0, gości: 1