Fórum Root.cz
Hlavní témata => Vývoj => Téma založeno: Arthur 18. 05. 2023, 16:44:59
-
Zdravím a mám dotaz na zkušenější Javisty:
Načítám číselná data ze souboru (~ stovky MB) do pole v paměti. Data mohou být různého formátu (int8, uint16, float32, ... atd) a proto volám v cyklu DataInputStream.readWhatever(), podle toho co je zrovna potřeba, např. takto:
DataInputStream dis = new DataInputStream(new BufferedInputStream(new FileInputStream(file)));
float[] pole = new float[size];
for (int i = 0; i < size; i++)
pole[i] = dis.readFloat();
Všechno funguje, až na to, že první volání tohoto kódu po spuštění aplikace je asi 3x pomalejší než všechna následná volání. Nezáleží na tom, jestli čtu ten samý soubor znovu nebo úplně jiný. Chová se to stejně i když ten soubor načtu do byte[] a použiju ByteArrayInputStream. Paměti je k dispozici řádově víc než velikost pole.
Pokud v tomto konkrétním případě použiju
byte[] buf = new byte[size * 4];
dis.read(buf);
ByteBuffer.wrap(buf).asFloatBuffer.getFloats(pole);
tak je to rychlostně OK, takže by to nemělo souviset s disk IO nebo alokací paměti, či co...
-
Vám vadí že první float načtete pomalu? Nebo tomu špatně rozumím? Pro rychlý přístup k datům můžete použít memory - mapped buffer, konkrétně např.
MappedByteBuffer.getFloat()
Viz:
- https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/nio/ByteBuffer.html#getFloat() (https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/nio/ByteBuffer.html#getFloat())
- https://www.baeldung.com/java-mapped-byte-buffer (https://www.baeldung.com/java-mapped-byte-buffer)
-
Co znamená „první volání je pomalejší“? Je pomalejší přečtení prvního celého streamu? Nebo je pomalé první čtení z každého streamu? Nebo první čtení z prvního streamu? Bude to stejně pomalé, když vynecháte DataInputStream nebo BufferedInputStream?
-
BufferedInoutStream alokuje buffer v konstruktoru a alokuje 8 KB. To by samo zpomalovat nemělo.
Pokud je to první volání I/O v programu, tak něco může spolknout první vytváření datových struktur pro spolupráci s operačním systémem.
Další možnost je, že při prvním průchodu se ten kód interpretuje a kompiluje a při dalším průchodu se používá kód již kompilovaný, tedy rychlejší. To by odpovídalo JVM typu hotspot, což je nejběžnější případ.
-
Pomalé je první čtení celého souboru. Mám datové soubory velikosti stovek MB a různě s nimi pracuju. S prvním souborem trvá načtení třeba 15s, poté už okolo 5s. Kdybych to mohl udělat jednoduše přes read(byte[]) v BufferedInputStream a potom jako celek zkonvertovat skrze ByteBuffer, tak je to asi za 2s (vždycky). Ale chci/potřebuju využívat ty metody specifické v DataIntputStreamu. Každopádně 5s je OK, potřebuju urychlit těch 15s.
Co třeba pomůže je načíst nejdřív malý soubor (2-3 MB) předem a pak už i ten první velký jde rychle. Ale když jsem zkusil takové iniciální "předčtení" přidat automaticky k načtení velkého souboru (načíst nejdřív malou část a pak znovu všechno), tak to nepomohlo.
Shrnuto:
DataInputStream: nejprve 15s a dále 5s
BufferedInputStream: nejprve 2s a dále 2s
Ano, je to první IO volání po spuštění
-
Pokud můžete, zkuste ten MappedByteBuffer. IMHO pro podobné použití ideální, přednačtete potřebné soubory a pak s nimi rychle pracujete, vše je v paměti, čímž eliminujete vliv filesystému apod.
-
OK díky, mrknu na to.
On by ten samotný ByteBuffer asi nakonec stačil, ale chtěl jsem využít mj. i DataInputStream.readUnsignedByte() a readUnsignedShort() abych se vyhnul ruční konverzi uint8 -> short a uint16 -> int. Předpokládám, že tyto vestavěné funkce jsou efektivnější než když to budu implementovat sám.. ale nakonec na výsledku uvidím
-
MappedByteBuffer plus vhodná konverzní/matematická/whatever knihovna - rychlosti bych se nebál, bitové operace a casting musí být rychlé, ale vhodná knihovna případně zpřehlední kód.
Nejvíc záleží co vlastne děláte, něco se dá prednacist, predpocitat, paralelizovat atd atd.
-
Je zpomalené čtení celého souboru, nebo třeba jen první čtení, nebo dokonce ještě před prvním čtením? Každopádně rozdíl 10 sekund je hodně. Když jde jen o první čtení, čekal bych, že tam bude alokace bufferů, ale to nemůže trvat 10 sekund, to je o mnoho řádů jinde. Pak mne ještě napadlo, zda do toho nezasahuje nějaký problém na straně OS, SElinux, NFS, čekání na nějaký timeout – ale pak zase nevidím důvod, proč by se to dělo jenom u prvního souboru.
-
Je zpomalené čtení celého souboru, nebo třeba jen první čtení, nebo dokonce ještě před prvním čtením?
Načítám do paměti celý soubor, viz:
DataInputStream dis = new DataInputStream(new BufferedInputStream(new FileInputStream(file)));
float[] pole = new float[size];
for (int i = 0; i < size; i++)
pole[i] = dis.readFloat();
dis.close();
Tohle trvá napoprvé těch 15s. Opakovaně už jen 5s se stejným nebo jiným souborem, třeba i se stejnou kopií toho prvního. Chová se to stejně na Win10 + Java8 Oracle, Ubuntu 20.04 /22.04 + Java11 openJDK. Poměr času je zachován, na rychlejším stroji je to 5s vs 1.7s.
Ještě mě napadlo jestli to nějak nemůže souviset s tím, že to pouštím v samostatném vlákně skrze java.util.concurrent.Executor, protože dokud je to uvnitř vlákna tak i opakované čtení je pořád stejně pomalé, ale jakmile to spustím znovu (v samostaném vlákně) tak už je to OK ...
-
Načítám do paměti celý soubor, viz:
Ano, ale můžete změřit dobu trvání každého řádku kódu. Jak dlouho trvá samotné vytvoření proměnné dis, tedy první řádek? Jak dlouho trvá každá obrátka cyklu, tedy volání readFloat()?
Tohle trvá napoprvé těch 15s. Opakovaně už jen 5s se stejným nebo jiným souborem, třeba i se stejnou kopií toho prvního. Chová se to stejně na Win10 + Java8 Oracle, Ubuntu 20.04 /22.04 + Java11 openJDK. Poměr času je zachován, na rychlejším stroji je to 5s vs 1.7s.
Takže to asi nebude věc OS, ale opravdu věc Javy.
Ještě mě napadlo jestli to nějak nemůže souviset s tím, že to pouštím v samostatném vlákně skrze java.util.concurrent.Executor
Pokud tím myslíte, že to spouštíte v jiném, než hlavním vlákně, to na to vliv nemá.
protože dokud je to uvnitř vlákna tak i opakované čtení je pořád stejně pomalé, ale jakmile to spustím znovu (v samostaném vlákně) tak už je to OK ...
Tomuhle nerozumím. Pokaždé to běží uvnitř nějakého vlákna. Jak ten vícevláknový kód vypadá?
-
a co otevirani souboru? co zkusit jine nacitaci (starsi) metody, a vyzkouset casy open, read, close atd. pripadne v c/c++ jak dlouho trva open souboru a pak prvni a dalsi ready.
-
Pokud něco trvá 10s, tak je potřeba zjistit co. Zda jde o pasivní čekání, nebo o aktivní činnost, na úrovni systému zda jde o vytížení v userspace nebo v kernelu, zda IO nebo CPU. Začal bych tím, že bych to normálně profiloval, sampler v Java mission console navede na pár kliknutí na úzká hrdla, která lze pak lépe zkoumat. Ještě mě napadá zda nedochází k nějakému vylistování adresáře - pokud tam je mnoho souborů, může to trvat velmi dlouho.
-
Našel jsem řešení, tedy spíše workaround (příčinu stále neznám):
Namísto
DataInputStream dis = new DataInputStream(new BufferedInputStream(new FileInputStream(file)));
float[] pole = new float[size];
for (int i = 0; i < size; i++)
pole[i] = dis.readFloat();
Použít
byte[] bytes = new byte[4];
ByteBuffer buf = ByteBuffer.wrap(bytes);
for (int i = 0; i < size; i++) {
dis.read(bytes);
buf.rewind();
pole[i] = buf.getFloat();
}
takže to zřejmě souvisí s implementací metod readXxx() v DataInputStreamu, v tomto konkrétním případě DataInputStream.read(byte[] bytes) je lepší než DataInputStream.readFloat()
-
Koukám do JDK:
DataInputStream.readFloat() volá interně DataInputStream.readInt, který 4x po sobě volá in.read() po bajtu, kde in je v tvém případě ten BufferedInputStream.
DataInputStream.read(bytes) to předává in.read(bytes), přičemž BufferInputStream.read(bytes, offset, len) volá svůj privátní read1(), která používá System.arraycopy().
Možná v tom je ten rozdíl.
-
byte[] bytes = new byte[4];
ByteBuffer buf = ByteBuffer.wrap(bytes);
for (int i = 0; i < size; i++) {
dis.read(bytes);
buf.rewind();
pole[i] = buf.getFloat();
}
Jen pozor, že InputStream.read nemusí vždycky naplnit celý buffer, pokud se mu nechce (a vrací počet bytů, který doopravdy načetl). Bezpečnější je použít něco jako třeba IOUtils.read z commons-io, ale je otázka, co to v tvém případě udělá s výkonem...
-
Od stolu bych tipoval: Volání BufferInputStream.read(bytes) na každý bytě je zpočátku neefektivní, ale JIT ho potom zoptimalizuje, proto při opakovaném použití to není problém. JIT typicky nekomplikuje kód hned, ale až po nějakém množství použití. Tím si jednak zajišťuje, že nekomplikuje kdeco, a jednak díky tomu má nějaké statistiky, kterých může využít. Takže část cyklu poprvé proběhne neoptimálně, proběhne kompilace a dál je to OK.
To, že záleží, ve kterém vlákně se to spustí, je na první pohled divné, ale v zásadě to není v rozporu s hypotézou. Mj. je možné, že i další vlákna budou mít nějaký vliv na JIT.
Jak to ověřit?
a) Napadá mě použití -Xint, tím JIT vypadne ze hry. Ano, bude to pomalejší, ale mělo by to být konzistentně pomalejší. Problém ale je, že to může být tak pomalé, že ten rozdíl v tom nebude patrný.
b) GraalVM native-image – vše se zkompiluje ještě před startem, JIT do toho nebude házet vidle. U dlouho běžících aplikací může JIT udělat lepší práci než native-image, ale native-image dává predikovatelnější výkon. Nevýhodou je, že nelze použít na každou aplikaci, protože to klade nějaké dodatečné požadavky – zejména musí vědět, co všechno má zkompilovat, do čehož mu může do nějaké míry házet vidle třeba reflexe provedená později než při statické inicializaci. Spoustu věcí lze řešit, jen jsem chtěl upozornit, že ne všechny aplikace lze takto přeložit na první dobrou.
c) Experimentovat s -Xcomp (zkompiluje vše při prvním použití; bude toho kompilovat více, bude častěji rekompilovat (protože častěji nevyjde spekulativní předpoklad), asi z toho nebudou padat tak dobré výsledky), případně -XX:CompileThreshold. Neříkám, že se to hodí do produkce, spíš to může přinést predikovatelnější chování.
BTW, Executor obecně nemusí kód spustit hned (a dokonce ani v jiném vlákně). Zřejmě s tím není problém (jinak by problém nevyřešil onen workaround), ale je dobré s tím počítat a nebrat volání Executor.execute(Runnable) za začátek práce.
-
byte[] bytes = new byte[4];
ByteBuffer buf = ByteBuffer.wrap(bytes);
for (int i = 0; i < size; i++) {
dis.read(bytes);
buf.rewind();
pole[i] = buf.getFloat();
}
Jen pozor, že InputStream.read nemusí vždycky naplnit celý buffer, pokud se mu nechce (a vrací počet bytů, který doopravdy načetl). Bezpečnější je použít něco jako třeba IOUtils.read z commons-io, ale je otázka, co to v tvém případě udělá s výkonem...
Ja by som navrhoval nemotať knižnice, kým netreba a použil dis.readFully(buff).
V každom prípade profiling, krátky test v JMH (i ked toto je relativne velka uloha pre takyto typ testu a hlavne zavisla od externych veci. Vysledok je teda kus otazny), alebo mozno aj spustenie cez strace a pozeranie ocami do konzoly, dokaze nieco odhalit. Prípadne nejaký z toolov z obrázku tuná https://brendangregg.com/linuxperf.html by mohol nasmerovat (nielen pri jave).
V kazdom pripade by som veril, ze 4 volania read() mozu byt ten rezdiel, co mate namerany. Skusit si oddedit vlastny kus a pretazit jednu metodu, aby to robila rovnak by mohlo byt vcelku jednoduche.
-
Načtení 128MB náhodných dat ze souboru jako float - hned na první průchod:
DataInputStream 2868 ms
MappedByteBuffer 632 ms
-
Díky za podněty, z uvedeného mi přijde, že hypotéza ohledně JIT nejvíce odpovídá reálnému chování.
Celkem uspokojivě jsem to vyřešil pomocí toho ByteBufferu. Čím větší tím je to rychlejší (a blíží se výkonu MappedByteBufferu), ale zase se potýkám s konzumací až 2-násobného množství RAM, což začíná být významné u souborů přes 500MB. Takže jsem zvolil nějaký přiměřeně velký buffer.