( asch | 2024. 02. 09., p – 10:33 )

Szerintem nem ment át, hogy mi a lényegi különbség amiről beszélek. A Java fordító úgy működik, hogy ahhoz, hogy lefordítsd az A.java osztályt ezt kell csinálnod:

 * Beparszolod a szöveget és felépíted az AST-t. (ezt inkrementális build esetén is nulláról meg kell csinálni, elvben az AST-t is karban lehetne tartani diffként, de ilyen megvalósításról nem tudok, normál esetben nem is éri meg, mert a forrásfájlok jó esetben kicsik.)

 * Az összes függőséget ki kell keresni (importok, package info, hivatkozott külső osztálynevek alapján ki kell keresni és be kell tölteni ezeket). A függőség lehet class formában (lefordított libre függés esetén), vagy .java formában, ha ugyanabban a build egységben van a függőség. A függőségek publikus API-ját be kell tölteni memóriába a fordító adatszerkezetébe! Ez az a lépés, amit egy memória rezidens inkrementális build ki tud spórolni. A függőségek nevei eleve indexelve a memóriában vannak, tehát a fájlokat sem kell újratölteni. Ezen belül az ösztályok API-ja a fordító belső adatszerkezete szerint beindexelve szintén a memóriában csücsül. Nem kell ezeket a .class és .java állományokat újraolvasni. Ezzel szemben ha a build folyamatot tiszta lappal indítod mindig, akkor ezt a lépést mindig újra kell csinálni!

Ha egy fájlt átírsz (IDE-t használva tipikus hogy minden fájlt azonnal mentünk amint szintaktikailag helyes a szerkesztésünk, és mentésre azonnal fut a build: a változás tehát mindig kicsi és csak 1 fájlt érint egyszerre.), akkor a fordító által végzendő munkának sokkal nagyobb része a függőségek újratöltése, mint az egyetlen fájl újrafordítása! Egyszerűen azért, mert a divatos módszertanok szerint az osztályok kicsik, viszont mivel minden kicsi, ezért a függőség meg jó sok darabra is. Ha a függőségek memória rezidensen őrizgetve vannak, akkor az újratöltést, tehát a munka nagyobb részét meg lehet spórolni.

(Illetve ha a szerkesztett osztályunk publikus API-ja változott, akkor az őrá épülő osztályokat is újra kell fordítani. De csak akkor ha az API változott, ha csak egy implementáció, akkor már nem! Ezt is akkor lehet egyáltalán követni, ha a fordítónak fejben megvan az előző fordítási állapot kimenete és ahhoz tudja viszonyítani az újat.)

Tehát simán a függőségi fa követése és csak a módosítások újrafordítása nem ekvivalens azzal, ha memória rezidens a fordító, és a saját struktúráiban tényleg csak azt frissíti be, amit szükséges. A második pont feladatait ugyanis így nem kell újra végigcsinálni, a munka oroszlánrésze megspórolható.

Egy Java és egy C fordítás nagyon másképpen működik, mivel Javában header fájlok helyett az implementációból van kitúrva a publikus API. De azért egy hasonlóságot fel lehet fedezni a "precompiled header" technika és a függőségek memóriában tartása technika között. A C világot jobban ismerők számára talán így lehet magyarázni a különbséget, hogy a függőségek memóriában tartásával kb azt lehet megnyerni, amit a precompiled headerrel lehet a C világban meg lehet nyerni. Ott is az van, hogy sok esetben a headerek parszolása többszöröse magának a forrásfájlnak a parszolásának, mivel a forráskódok a divat szerint kicsit, a gyári headerek viszont az összes szabványos csellentyűcskét tartalmazzák és ezért jó komplexek.

A gradle leírása alapján én úgy értem, hogy a fordító implementációt egy bemelegített JVM-ben tudja tartani, ami gyorsít az induláson (lásd JIT problémakör), de a fordító belső állapotát nem őrzi meg két hívás között. A linkelt leírások mind erre utalnak. De szívesen tanulok újat, ha rámutattok arra, hogyan őrzi meg a gradle a fordítás belső állapotát két inkrementális build között.