tifyty

pure Java, what else ?

Lassú-e a thread local?

Általában azt szoktuk mondani, hogy a túl korai optimalizálás minden gonosz forrása. Ezen már sok házasság is tönkrement, és különböző orvosok is előállnak mindenféle javaslatokkal, mint a láb hideg vízbe dugása, gondoljunk közben másra stb. Vannak azonban olyan esetek, amikor felül kell bírálni ezt az általános állítást. Az egyik az architektúra és algoritmus tervezés.

Ha az architektúra tervezésnél nem törődöm a teljesítménnyel, akkor utólag már profilozhatom a kódot, mint a krimiben a nyomozó a pszichopata sorozatgyilkost: a való életben nem segít a dolog. A halott nem fog feltámadni. Nyögünk az alulteljesítő rendszerrel, vagy újratervezzük, és írjuk az egészet, vagy rossz esetben a versenytársunk teszi ezt meg.

Széljegyzet: Én azt gondolom, hogy az architektúra tervezése során az optimalizálás nem a “túl korai” kategória. Természetesen csak a maga szintjén. Így ez nem dönti romba a fenti általános érvényű szabályt. Optimalizálni több szinten kell. Architektúrát az architektúra tervezés (nem túl korai) szintjén. Kódot meg később.

Van azonban még egy történet, amelyik persze megítéléstől függően szintén nem rombolja le a fenti állítást, és ez a másik történet már kódoláskor jön elő. Ez pedig az, hogy nem használunk olyan konstrukciókat, algoritmusokat és algoritmus implementációkat, amelyek nem felelnek meg általában a követelményeink jellegének. Ezt nem feltétlenül nevezném optimalizálásnak. Csupán arról van szó, hogy nem írunk szar kódot.

Például ha másodpercenként tízezres nagyságrendű tranzakciót kell végrehajtanunk egy sokprocesszoros gépen, és minden tranzakciónak garantált időn belül le kell futnia, akkor nem használunk HashMap-et, még akkor sem, ha egy szálon belül használjuk csak egy időpillanatban. Miért? Nos azért, mert a HashMap hajlamos arra, hogy néha újraszervezze magát. Megtelik, és úgy érzi, hogy nagyon puffad. Ilyenkor megnöveli a kétszeresére a map méretét (ha tudja), és mivel ilyenkor minden indexelés megváltozik, ezért minden egyes elemet átpakol az új helyére. Ez időbe kerül. Nem kevés időbe, és főleg nem prediktív, nem lehet megmondani, hogy mikor fog bekövetkezni, melyik lesz az a thread, tranzakció, amelyik éppen nem fut le, csak mondjuk 20ms késleltetéssel. Persze az egész Java a GC miatt nem igazi real time, de ennek ellenére olyan esetekben, ahol csak pénz, és nem emberélet függ attól, hogy mennyire real time válaszol az alkalmazás van a használatra bőven példa.

Ez a témakör egyébként a szemétgyűjtés is, amelyik a mai (2012) architektúrákon, és a 3GHz processzor órajelek mellett egy másodperc minden kipucolt gigabájtra. De az egy másik téma, amiről csak olvastam, nem volt eddig vele direkt tapasztalatom, olvasni meg más is tud, akit érdekel nézze meg, mit mond magáról az Azul, hogyan oldja meg, hogy ne csukoljon (össze) a szemétgyűjtés alatt a JVM. (Csodát persze ők sem tudnak tenni, valamit valamiért; de azért jópofa, amit csinálnak. Jópofa? Annál azért kicsit több.)

Minap azt mondta nekem valaki, hogy ilyen rendszerekben nem használunk ThreadLocal változókat, mert az hash alapú map, és mint tudjuk az lassú (lehet).

Valóban az lehet, ha nagyon megtömjük. De meg akarjuk-e tömni? (Már megint ennél a témánál vagyunk, mint a cikk elején?) A konkrét esetben egyetlen szál helyi változóról lett volna szó. Egyetlen egyről, és garantált lett volna, hogy az alkalmazás másik része sem használ másik ThreadLocal változót. Amúgy aztán az egész megoldás, amelyik a ThreadLocal használatát igényelte volna zsákutca volt, de egészen más okok miatt. Ennek ellenére a gondolat befészkelte magát a fejembe, és ha valami fészket rak, akkor az általában tojásokat is, ami ki is fog kelni. (Már megint a szex.) Szóval jöttek újabb gondolatok…

Azt gondoltam, hogy ha lassú a ThreadLocal akkor megnézem, hogy pontosan mi is a gond vele, és hogyan lehetne valami olyant készíteni, ami helyettesítheti és nem lassú. És azt találtam…. És akkor itt lassítsunk egy kicsit.

Mi is az a ThreadLocal ?

private static ThreadLocal<Object> alma = new ThreadLocal<>();

A ThreadLocal általában egy private static változó, amelyiknek a set() metódusával tudunk beállítani egy értéket, amit utána a get() metódussal érünk el. A speciális benne, hogy ha két különböző szál, akár konkurensen meghívja a set() metódust, mind a kettő később a get() metódus meghívásakor a saját objektumát kapja vissza.

Hogyan történik ez? Úgy, hogy magában a Thread objektumban van egy map (a neve threadLocals ha valaki bele akar túrni a JDK kódba), amelyikbe a set() beleteszi az argumentumként megkapott értéket, és mivel ez a map minden egyes szál sajátja, a get() minden szálnak a saját objektumát adja vissza. Van egy kis csavar a dologban, hogy mi a ThreadLocal set() és get() metódusát hívjuk meg, de az elkéri a Thread.currentThread() hívással az aktuális szálat, és onnan kezdve már megvan a szál map-ja is. A map indexeléséhez pedig mi mást használna, mint magát a ThreadLocal objektumot.

Mi lehet ebben lassú? A map maga létrejön amikor az első set() hívás az adott szálon végrehajtódik. Mégpedig ezzel a konstruktorral:

        ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

Ez nem tűnik lassúnak, és ha az is lenne, csak egyszer hívódik meg, amikor a statikus ThreadLocal változónak értéket adunk. INITIAL_CAPACITY értéke 16, így még a modulo számítása is igen gyorsra van faragva egy bit művelettel. Ez nem lassú. A map kezelés pedig eléggé speciális, és minden egyes része arra utal (lehet tanulni a kódból), hogy elég rendesen végigelemezték, hogy ne legyen lassú.

Ott kezdődik, hogy hash számítása le van egyszerűsítve. Minek elbonyolítani? A map-hez a ThreadLocal nem a szokásos hash kódot használja, mert annak a számítása lassú (lehet), hanem a

    private final int threadLocalHashCode = nextHashCode();
    private static AtomicInteger nextHashCode =
        new AtomicInteger();
    private static final int HASH_INCREMENT = 0x61c88647;
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

kódot. Szóval ez nem lesz lassú. Mit csinál a set() és a get() ? Ha nincs táblabetelés, és nincs hash kód egybeesés (mert pl. csak pontosan egy ThreadLocal változónk van) akkor egyszerűen kiveszi az entry tömb hash értékének megfelelő elemet. A JDK forráskódban minden más varázslás csak akkor kezdődik és fut, amikor hash kód egybeesés van, vagy kezd betelni a tábla.

Vagyis a ThreadLocal nem lassú, ha kordában tudjuk tartani az étvágyunkat, és nem használunk belőle sokat. Mondjuk maximum 9-et.

Ki mondja meg, hogy miért pont 9-et? Ha más senki, akkor egy hét múlva én.

4 responses to “Lassú-e a thread local?

  1. liptga december 21, 2012 11:47 de.

    Mert a java.lang.ThreadLocal.ThreadLocalMap.setThreshold(int) a 16 initialcapacity 2/3-ára állítja a rehash limitet, ami 10.66, tehát a java.lang.ThreadLocal.ThreadLocalMap.set(ThreadLocal, Object) metódusban 10-es méretnél már rehashel. Nyertem?

Vélemény, hozzászólás?

Adatok megadása vagy bejelentkezés valamelyik ikonnal:

WordPress.com Logo

Hozzászólhat a WordPress.com felhasználói fiók használatával. Kilépés / Módosítás )

Twitter kép

Hozzászólhat a Twitter felhasználói fiók használatával. Kilépés / Módosítás )

Facebook kép

Hozzászólhat a Facebook felhasználói fiók használatával. Kilépés / Módosítás )

Google+ kép

Hozzászólhat a Google+ felhasználói fiók használatával. Kilépés / Módosítás )

Kapcsolódás: %s

%d blogger ezt kedveli: