0xDEADBEEF

[RSS]

Jak rychlá je reflexe na JVM?

25. 8. 2019

Když píšete interpretr běžící na JVM, je velice užitečné, aby uměl používat již existující knihovny napsané v Javě. K tomu ale musí umět volat do Javy, což v případě, že váš jazyk je dynamicky typovaný (třeba nějaká varianta Lispu), nemusí být triviální a/nebo rychlé. Volání může proběhnout několika způsoby:

  1. vygenerovat bytekód, který adaptuje argumenty a zavolá správnou metodu.
  2. klasická reflexe java.lang.reflect.Method
  3. java.lang.invoke.MethodHandle
  4. java.beans.Expression

Poslední jmenovaná je rozhodně nejpohodlnější varianta. Ale jak rychlá je v porovnání s ostatními? Napsal jsem proto tento maličký JMH benchmark testující jednotlivé metody volání.

class X {
  def method() = 1
}

class MethodCalls {
  val obj = new X
  val method = obj.getClass.getDeclaredMethod("method")
  val method2 = {
    val m = obj.getClass.getDeclaredMethod("method")
    m.setAccessible(true)
    m
  }

  val mh: MethodHandle = MethodHandles.lookup.unreflect(method)

  @Benchmark def directCall: Int =
    obj.method

  @Benchmark def reflection: Int =
    obj.getClass.getDeclaredMethod("method").invoke(obj).asInstanceOf[Int]

  @Benchmark def reflectionReused: Int =
    method.invoke(obj).asInstanceOf[Int]

  @Benchmark def reflectionAccessible: Int =
    method2.invoke(obj).asInstanceOf[Int]

  @Benchmark def beans: Int =
    new java.beans.Expression(obj, "method", Array()).getValue.asInstanceOf[Int]

  @Benchmark def methodHandle: Int =
    (mh.invokeExact(obj): Int)
}

Výsledky na OpenJDK 11.0.4 a věrném procesoru i5-4570 jsou následující:

Benchmark                              Score   Error  Units
MethodCalls.directCall                 2,559 ± 0,004  ns/op
MethodCalls.reflection                41,441 ± 0,063  ns/op
MethodCalls.reflectionReused           6,259 ± 0,046  ns/op
MethodCalls.reflectionAccessible       5,192 ± 0,011  ns/op
MethodCalls.beans                   1259,326 ± 0,998  ns/op
MethodCalls.methodHandle               5,692 ± 0,067  ns/op

Dostatečně inteligentní a agresivní JIT by teoreticky mohl zoptimalizovat všechny verze na úroveň lepší reflexe, kdyby inlinoval kompletní graf volání a nasekal tam fůru inline chache. V tomto kontextu by mohlo být zajímavé zjistit, jaké časy vyprodukuje GraalVM.


MethodHandle podává lepší výsledky, pokud volám metodu s primitivními parametry.

Stejný benchmark, změnilo se jen tohle:

class X {
  private[this] val str = "EVENDEEPER"
  def method(pos: Int): Int = str.charAt(pos)
}

@Benchmark def directCall: Int =
  obj.method(4)

@Benchmark def reflectionReused: Int =
  method.invoke(obj, new Integer(4)).asInstanceOf[Int]

@Benchmark def methodHandle: Int =
  (mh.invokeExact(obj, 4): Int)

Výsledky jsou následující:

Benchmark                              Score   Error  Units
MethodCalls.directCall                 5,701 ± 0,116  ns/op
MethodCalls.methodHandle              10,925 ± 0,003  ns/op
MethodCalls.reflectionReused          19,670 ± 0,349  ns/op

MethodHandle je (IMHO) rychlejší, protože nemusí boxovat a unboxovat primitivní typy.


Ale jako vždy nezapomeňte, jde jen o čísla, která nohou mít s realitou běžných programů společného o dost méně, než by se nám líbilo. Benchmarkování dynamicky kompilujících virtuálních strojů je komplikované.

21. 5. 2019Scala, mapy a čítače
13. 5. 2019Další novinky ve Scale 2.13
14. 4. 2019Parciální funkce ve Scale a dvojité vyhodnocování
10. 4. 2019Minimalistické zvýrazňování syntaxe
13. 3. 2019Jak rychlý je čas (v Javě)?
21. 2. 2019Really simple RSS
26. 1. 2019Java, Scala a regulární výrazy #6 - znovupoužití Matcher objektu
12. 1. 2019Poznámky ke slajdům k přednášce, kterou jsem nikdy nepřednášel
4. 1. 2019Minifikace HTML5
24. 12. 2018Java IO & NIO - jak nejrychleji načíst soubor z disku
3. 11. 2018Průnik množin ve Scale
23. 10. 2018Čím nahradit Scala.XML
11. 10. 2018Novinky kolekcí ve Scale 2.13
4. 8. 2018Co vlastně benchmarkujete?
28. 7. 2018Java, Scala a regulární výrazy #5 - posesivní regexy a StackOverflowError
26. 7. 2018EDGE, TRIPS a hon za vyšším ILP
24. 6. 2018Striktní a líné jazyky
21. 6. 2018Syntéza datových struktur a programů
9. 6. 2018ISPC, SPMD a SIMD
6. 4. 2018Konec Intelu, konec Moorova zákona, konec světa takového jaký známe
31. 1. 2018Meltdown, Spectre a branch prediction
28. 12. 2017Poznámky k výkonu
19. 12. 2017Java, Scala a regulární výrazy #4 - líné regexy a StackOverflowError
5. 12. 2017Java, Scala a regulární výrazy #3 - rychlost
27. 11. 2017Java, Scala a regulární výrazy #2 - rychlost
26. 11. 2017Java, Scala a regulární výrazy
21. 11. 2017Java - zostřování obrázků
7. 10. 2017Koherence cache a atomické operace
5. 10. 2017Nejrychlejší hashmapa pod sluncem
23. 9. 2017Dekódování x86 instrukcí
21. 9. 2017Energetická efektivita programovacích jazyků
19. 9. 2017Programming and Performance podcast
17. 9. 2017B-stromy, persistence, copy-on-write a Btrfs
15. 9. 2017Rust a parsery
13. 9. 2017Velké stránky paměti
11. 9. 2017Deanonymizace agregovaných dat
9. 9. 2017Optimalizující kompilátor
7. 9. 2017Identifikace zašifrovaných videí
5. 9. 2017Heterogenní persistentní kolekce
3. 9. 2017JVM Anatomy Park
1. 9. 2017Datacentrum z mobilů
30. 8. 2017Propustnost pamětí
28. 8. 2017Branch prediction (implementace v procesorech)
Starší články byly publikovány na funkcionálně.cz.
píše k47 (@kaja47, k47)