Scala 3 a klíčové slovo inline
Nedávno vyšla třetí verze Scaly a přinesla s sebou mnoho velkých změn.
Dnes se chci zaměřit jen na jednu z nich: klíčové slovo inline. To umožňuje
používat abstrakce s nulovou režií a zároveň představují bránu do světa maker.
Scala v minulosti měla asi tři nezávislé verze makrosystému a teprve tenhle
vypadá konečně příčetně.
Mě na inline zajímá především garance, že abstrakce budou zcela eliminovány a výsledkem bude stejný bytekód, jako kdyby smyčky byly napsané ručně. Techniky
kombinující specializace a type class nejsou na 100% spolehlivé, ale
klíčové slovo inline garantuje, že tělo metody bude inlinováno do
místa odkud se volá.
S jeho pomocí můžu napsat obecnou inline funkci pro redukci pole
inline def op0[T](init: T, inline f: (T, T) => T, arr: Array[T]): T = { var s = init var i = 0; while (i < arr.length) { s = f(s, arr(i)) i += 1 } s }
a pak příslušné konkrétní funkce specializovaná pro daný typ a operaci.
def sum(arr: Array[Double]) = op0(0.0d, _ + _, arr) def sum(arr: Array[Float]) = op0(0.0f, _ + _, arr) def product(arr: Array[Double]) = op0(1.0d, _ * _, arr) def product(arr: Array[Float]) = op0(1.0f, _ * _, arr)
Do každé z nich bude vloženo tělo op0 a výsledek je bytekód bez jakékoli
abstrakce. Není v něm žádné volání anonymních funkcí, žádná alokace, nic. Vše
inlinováno do podoby prosté smyčky.
inline u argumentu f způsobí, že i tělo funkce předané jako argument bude
inlinováno a po volání anonymní funkce nezbudou ani stopy. Díky dvojí inline
anotaci v programu nezůstane žádný invokevirtual nebo invokedynamic.
Můžu třeba snadno a bez použití maker vytvořit obdobu klasické for
smyčky, která ve Scale jinak chybí. Je u ní garantováno, že poběží
stejně rychle jako, kdyby ji jazyk měl a nezáleží, jak úspěšný bude JIT při
optimalizaci.
inline def forloop[T](inline init: T)(inline cond: T => Boolean, inline incr: T => T)(inline op: T => Unit) = { var i = init; while (cond(i)) { op(i) i = incr(i) } }
Použití není tak přirozené, jako kdyby šlo o zabudovaný konstrukt, ale lepší
než while u níž rád zapomínám na i += 1 a pak čekám, kdy nekonečná smyčka
skončí.
def sum(xs: Array[Double]) = { var s = 0.0 // for (var i = 0; i < xs.length; i += 1) forloop(0)(_ < xs.length, _ + 1) { i => s += xs(i) } s }
Výsledný bytekód je zase prostý jakékoli abstrakce. V tomto případě došlo k eliminování alokace DoubleRef, která je nutná v situacích, kdy closure
mění lokální proměnnou s.
Jako o něco komplikovanější příklad vezmu stejnou funkci jako tady. Počítá velikost průniku množin reprezentovaných jako seřazené pole.
def intersectionSize[T: Numeric](a: Array[T], b: Array[T]): Int
Jak ji napsat, aby bylo zaručeno, že vždy poběží stejně efektivně jako v případě ruční specializace pro každý typ?
V odkazovaném článku jsem použil kombinaci specializace a type class. JIT může, pokud je
specializovaná nejen funkce sama, ale i metody na type class, vše pěkně
inlinovat, ale není garantováno, že vždy uspěje. inline poskytne záruky.
def intersectionSize(a: Array[Short], b: Array[Short]): Int = intersectionSizeBody(a, b, ShortOrd) inline def intersectionSizeBody[T](a: Array[T], b: Array[T], inline ord: Ord[T]): Int = { var ai, bi, c = 0; while (ai < a.length && bi < b.length) { val aa = a(ai) val bb = b(bi) c += (if (ord.eq(aa, bb)) 1 else 0); ai += (if (ord.lteq(aa, bb)) 1 else 0); bi += (if (ord.gteq(aa, bb)) 1 else 0); } c } trait Ord[T] { def eq(a: T, b: T): Boolean def lteq(a: T, b: T): Boolean def gteq(a: T, b: T): Boolean } object ShortOrd extends Ord[Short] { inline def eq(a: Short, b: Short): Boolean = a == b inline def lteq(a: Short, b: Short): Boolean = a <= b inline def gteq(a: Short, b: Short): Boolean = a >= b }
intersectionSize je konkrétní instanciace pro daný typ, inline funkce
intersectionSizeBody je expandována všude, kde je použita. Anotace argumentu
ord jako inline znamená, že se jeho instance zkopíruje na všechna místa
použití. To spolu s inline def metodami na type class Ord způsobí, že
jejich těla se plně inlinují do místa volání Ve výsledku je všechna abstrakce
kompletně eliminována již na straně scalac.
Nová inline funkce svou silou téměř dosahují síly templatování. Ne úplně, kód musí typechecknout před instanciací.
transparent inline def x0[T](a: T) = a + 1 transparent inline def x1(a: Any) = a + 1
Ve Scale ani jedno neprojde, zatímco klasické templatování àla D by definice povolilo a kompilátor by si stěžoval až v okamžiku instanciace, kdy typ nemá příslušnou metodu.
Templatováním zavání funkce typeChecks kontrolující, jestli je předaný jako
string validní v daném (inlinovaném) kontextu.
inline if (scala.compiletime.testing.typeChecks("val x: Short = array(0)")) {}
Tohle (ve spojení s transparent metodami) je překvapivě mocný nástroj, jež
bez psaní maker dovoluje staticky podnikat kroky obcházejí typový systém a lidé
jistě přijdou na tisíc a jeden způsob jak to využít a zneužít pro fantastické
věci.
U příležitosti nové verze jsem taky aktualizoval několik velice starých článků publikovaných dva blogy nazpátek: Zřetězené porovnávání, Klasický for cyklus, Postfixový if, Operátor mocniny, Booleovská kompozice funkcí.