Java >> Java tutorial >  >> Java

Affaldsfri kodning

Denne artikel dokumenterer løsning af et meningsfuldt hændelsesbehandlingsproblem på en yderst effektiv måde gennem reduktion af spild i softwarestakken.

Java ses ofte som et hukommelsessvin, der ikke kan fungere effektivt i miljøer med lav hukommelse. Målet er at demonstrere, hvad mange tror er umuligt, at et meningsfuldt java-program kan fungere i næsten ingen hukommelse. Eksempelprocesserne
2,2 millioner csv-poster pr. sekund i en 3MB-dynge med nul gc på en enkelt tråd i Java .

Du vil lære, hvor hovedområderne for affald findes i en java-applikation, og de mønstre, der kan bruges til at reducere dem. Konceptet med nulomkostningsabstraktion introduceres, og at mange optimeringer kan automatiseres på kompileringstidspunktet gennem kodegenerering. Et Maven-plugin forenkler udviklerens arbejdsgang.

Vores mål er ikke høj ydeevne, der kommer som et biprodukt af at maksimere effektiviteten. Løsningen anvender Fluxtion, som bruger en brøkdel af ressourcerne sammenlignet med eksisterende Java-hændelsesbehandlingsrammer.

Computing og klimaet

Klimaændringer og deres årsager er i øjeblikket af stor bekymring for mange. Computere er en vigtig kilde til emissioner og producerer det samme CO2-fodaftryk som hele flyindustrien. I mangel af regulering, der dikterer computerenergiforbruget, må vi som ingeniører påtage os ansvaret for at producere effektive systemer afbalanceret mod omkostningerne for at skabe dem.

På en panelsession fra infoq 2019 i London talte Martin Thompson lidenskabeligt om at bygge energieffektive computersystemer. Han bemærkede, at kontrol med affald er den kritiske faktor for at minimere energiforbruget. Martins kommentarer gav genklang hos mig, da kernefilosofien bag Fluxtion er at fjerne unødvendigt ressourceforbrug. Den panelsession var inspirationen til denne artikel.

Behandlingskrav

Krav til behandlingseksemplet er:

  • Operer i 3 MB heap med nul gc
  • Brug kun standard java-biblioteker, ingen "usikre" optimeringer
  • Læs en CSV-fil, der indeholder millioner af rækker af inputdata
  • Input er et sæt ukendte hændelser, ingen forudindlæsning af data
  • Datarækker er heterogene typer
  • Behandle hver række for at beregne flere samlede værdier
  • Beregninger er betinget af rækketypen og dataindholdet
  • Anvend regler på aggregater og tæl regelbrud
  • Data er tilfældigt fordelt for at forhindre forudsigelse af gren
  • Partitionsberegninger baseret på rækkeinputværdier
  • Samle og grupper opdelte beregninger i en samlet visning
  • Udgiv en oversigtsrapport i slutningen af ​​filen
  • Ren Java-løsning med funktioner på højt niveau
  • Ingen JIT-opvarmning

Eksempel på overvågning af position og overskud

CSV-filen indeholder handler og priser for en række aktiver, én post pr. række. Position og fortjenesteberegninger for hvert aktiv er opdelt i deres eget hukommelsesrum. Aktivberegninger opdateres ved hver matchende inputhændelse. Overskud for alle aktiver vil blive aggregeret til et porteføljeoverskud. Hvert aktiv overvåger sin aktuelle position/profit-tilstand og registrerer en optælling, hvis en af ​​dem overtræder en forudindstillet grænse. Porteføljens overskud vil blive overvåget, og tabsbrud tælles.

Regler valideres på aktiv- og porteføljeniveau for hver indkommende begivenhed. Antallet af regelbrud opdateres, efterhånden som hændelser streames ind i systemet.

Rækkedatatyper

href="https://github.com/gregv12/articles/blob/article_may2019/2019/may/trading-monitor/src/main/java/com/fluxtion/examples/tradingmonitor/AssetPrice.java" target="_blank" rel="noopener noreferrer">AssetPrice - [price: double] [symbol: CharSequence]

Deal       - [price: double] [symbol: CharSequence] [size: int]

Eksempel på data

CSV-filen har en overskriftslinje for hver type for at tillade dynamisk kolonneposition til felttilknytning. Forud for hver række står det simple klassenavn på den måltype, der skal samles ind i. Et eksempelsæt af poster inklusive header:

Deal,symbol,size,price
AssetPrice,symbol,price
AssetPrice,FORD,15.0284
AssetPrice,APPL,16.4255
Deal,AMZN,-2000,15.9354

Beregningsbeskrivelse

Aktivberegninger opdeles efter symbol og samles derefter i en porteføljeberegning.

Opdelte aktivberegninger

asset position  = sum(Deal::size)
deal cash value = (Deal::price) X (Deal::size) X -1
cash position   = sum(deal cash value)
mark to market  = (asset position) X (AssetPrice::price)
profit          = (asset mark to market) + (cash position)

Porteføljeberegninger

portfolio profit = sum(asset profit)

Overvågningsregler

asset loss > 2,000
asset position outside of range +- 200
portfolio loss > 10,000

BEMÆRK:

  1. Der foretages en optælling, når en anmelder angiver et regelbrud. Meddeleren skyder kun på det første brud, indtil det er nulstillet. Meddeleren nulstilles, når reglen bliver gyldig igen.
  2. En positiv handel::størrelse er et køb, en negativ værdi et salg.

Eksekveringsmiljø

For at sikre, at hukommelseskravene er opfyldt (nul gc og 3MB heap),
Epsilon no-op garbage collector bruges, med en max bunkestørrelse på 3MB. Hvis der er allokeret mere end 3 MB hukommelse i hele processens levetid, afsluttes JVM med det samme med en fejl i hukommelsen.

For at køre prøven:klon fra git  og i roden af ​​trading-monitor-projektet kør jar-filen i dist-mappen for at generere en testdatafil på 4 millioner rækker.

git clone --branch  article_may2019 https://github.com/gregv12/articles.git
cd articles/2019/may/trading-monitor/
jdk-12.0.1\bin\java.exe -jar dist\tradingmonitor.jar 4000000

Som standard behandler tradingmonitor.jar filen data/generated-data.csv. Ved at bruge kommandoen over skal inputdataene have 4 millioner rækker og være 94MB i længden klar til udførelse.

Resultater

For at udføre testen skal du køre tradingmonitor.jar uden argumenter:

jdk-12.0.1\bin\java.exe -verbose:gc -Xmx3M -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -jar dist\tradingmonitor.jar

Ved at udføre testen for 4 millioner rækker er de sammenfattende resultater:

Process row count     =    4 million
Processing time       =    1.815 seconds
Avg row exec time     =  453 nano seconds
Process rate          =    2.205 million records per second
garbage collections   =    0
allocated mem total   = 2857 KB
allocated mem per run =   90 KB
OS                    = windows 10
Processor             = Inte core [email protected]
Memory                = 16 GB
Disk                  = 512GB Samsung SSD PM961 NVMe

BEMÆRK:Resultaterne er fra det første løb uden JIT-opvarmning. Efter jit warmup er kodeudførelsestiderne ca. 10% hurtigere. Samlet allokeret hukommelse er 2,86 Mb, hvilket inkluderer start af JVM.

Ved at analysere Epsilons output anslår vi, at appen allokerer 15 % hukommelse til 6 kørsler eller 90 KB pr. Der er en god chance for, at applikationsdataene passer ind i L1-cachen, flere undersøgelser er påkrævet her.

Output

Testprogrammet går i løkker 6 gange og udskriver resultaterne hver gang, Epsilon registrerer hukommelsesstatistik ved slutningen af ​​kørslen.

jdk-12.0.1\bin\java.exe" -server -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC  -Xmx3M -verbose:gc -jar dist\tradingmonitor.jar
[0.011s][info][gc] Non-resizeable heap; start/max: 3M
[0.011s][info][gc] Using TLAB allocation; max: 4096K
[0.011s][info][gc] Elastic TLABs enabled; elasticity: 1.10x
[0.011s][info][gc] Elastic TLABs decay enabled; decay time: 1000ms
[0.011s][info][gc] Using Epsilon
[0.024s][info][gc] Heap: 3M reserved, 3M (100.00%) committed, 0M (5.11%) used
[0.029s][info][gc] Heap: 3M reserved, 3M (100.00%) committed, 0M (10.43%) used
.....
.....
[0.093s][info][gc] Heap: 3M reserved, 3M (100.00%) committed, 1M (64.62%) used
[0.097s][info][gc] Heap: 3M reserved, 3M (100.00%) committed, 2M (71.07%) used


portfolio loss gt 10k count -> 792211.0
Portfolio PnL:-917.6476000005273
Deals processed:400346
Prices processed:3599654
Assett positions:
-----------------------------
[1.849s][info][gc] Heap: 3M reserved, 3M (100.00%) committed, 2M (76.22%) used
MSFT : AssetTradePos{symbol=MSFT, pnl=484.68589999993696, assetPos=97.0, mtm=1697.0247000000002, cashPos=-1212.3388000000632, positionBreaches=139, pnlBreaches=13628, dealsProcessed=57046, pricesProcessed=514418}
GOOG : AssetTradePos{symbol=GOOG, pnl=-998.6065999999155, assetPos=-1123.0, mtm=-19610.1629, cashPos=18611.556300000084, positionBreaches=3, pnlBreaches=105711, dealsProcessed=57199, pricesProcessed=514144}
APPL : AssetTradePos{symbol=APPL, pnl=-21.881300000023202, assetPos=203.0, mtm=3405.1017, cashPos=-3426.9830000000234, positionBreaches=169, pnlBreaches=26249, dealsProcessed=57248, pricesProcessed=514183}
ORCL : AssetTradePos{symbol=ORCL, pnl=-421.9756999999504, assetPos=-252.0, mtm=-4400.4996, cashPos=3978.5239000000497, positionBreaches=103, pnlBreaches=97777, dealsProcessed=57120, pricesProcessed=513517}
FORD : AssetTradePos{symbol=FORD, pnl=112.14559999996254, assetPos=-511.0, mtm=-7797.8089, cashPos=7909.9544999999625, positionBreaches=210, pnlBreaches=88851, dealsProcessed=57177, pricesProcessed=514756}
BTMN : AssetTradePos{symbol=BTMN, pnl=943.8932999996614, assetPos=-1267.0, mtm=-19568.9417, cashPos=20512.83499999966, positionBreaches=33, pnlBreaches=117661, dealsProcessed=57071, pricesProcessed=514291}
AMZN : AssetTradePos{symbol=AMZN, pnl=-557.0849999999355, assetPos=658.0, mtm=10142.214600000001, cashPos=-10699.299599999937, positionBreaches=63, pnlBreaches=114618, dealsProcessed=57485, pricesProcessed=514345}
-----------------------------
Events proecssed:4000000
millis:1814
...
...
portfolio loss gt 10k count -> 792211.0
Portfolio PnL:-917.6476000005273
Deals processed:400346
Prices processed:3599654
Assett positions:
-----------------------------
MSFT : AssetTradePos{symbol=MSFT, pnl=484.68589999993696, assetPos=97.0, mtm=1697.0247000000002, cashPos=-1212.3388000000632, positionBreaches=139, pnlBreaches=13628, dealsProcessed=57046, pricesProcessed=514418}
GOOG : AssetTradePos{symbol=GOOG, pnl=-998.6065999999155, assetPos=-1123.0, mtm=-19610.1629, cashPos=18611.556300000084, positionBreaches=3, pnlBreaches=105711, dealsProcessed=57199, pricesProcessed=514144}
APPL : AssetTradePos{symbol=APPL, pnl=-21.881300000023202, assetPos=203.0, mtm=3405.1017, cashPos=-3426.9830000000234, positionBreaches=169, pnlBreaches=26249, dealsProcessed=57248, pricesProcessed=514183}
ORCL : AssetTradePos{symbol=ORCL, pnl=-421.9756999999504, assetPos=-252.0, mtm=-4400.4996, cashPos=3978.5239000000497, positionBreaches=103, pnlBreaches=97777, dealsProcessed=57120, pricesProcessed=513517}
FORD : AssetTradePos{symbol=FORD, pnl=112.14559999996254, assetPos=-511.0, mtm=-7797.8089, cashPos=7909.9544999999625, positionBreaches=210, pnlBreaches=88851, dealsProcessed=57177, pricesProcessed=514756}
BTMN : AssetTradePos{symbol=BTMN, pnl=943.8932999996614, assetPos=-1267.0, mtm=-19568.9417, cashPos=20512.83499999966, positionBreaches=33, pnlBreaches=117661, dealsProcessed=57071, pricesProcessed=514291}
AMZN : AssetTradePos{symbol=AMZN, pnl=-557.0849999999355, assetPos=658.0, mtm=10142.214600000001, cashPos=-10699.299599999937, positionBreaches=63, pnlBreaches=114618, dealsProcessed=57485, pricesProcessed=514345}
-----------------------------
Events proecssed:4000000
millis:1513
[14.870s][info][gc] Total allocated: 2830 KB
[14.871s][info][gc] Average allocation rate: 19030 KB/sec

Affaldshotspots

Tabellen nedenfor identificerer funktioner i behandlingssløjfen, der traditionelt skaber spild og affaldsforebyggelsesteknikker, der anvendes i eksemplet.

Funktion Kilde til affald Effekt Undgåelse
Læs CSV-fil Tildel en ny streng for hver række GC Læs hver byte ind i en flyvevægt og bearbejd i allokeringsfri dekoder
Dataholder for række Tildel en dataforekomst for hver række GC Flyweight enkelt dataforekomst
Læs kolonneværdier Tildel en række strenge for hver kolonne GC Skub tegn ind i en genanvendelig tegnbuffer
Konverter værdi til type String to type-konverteringer tildeler hukommelse GC Nulallokeringskonvertere CharSequence i stedet for Strings
Skub col-værdi til holderen Autoboxing for primitive typer allokerer hukommelse. GC Primitive bevidste funktioner pusher data. Nul tildeling
Partitioneringsdatabehandling Datapartitioner behandles parallelt. Opgaver tildelt til køer GC / Lås Behandling af enkelt tråd, ingen tildeling eller låse
Beregninger Autoboxing, uforanderlige typer, der allokerer mellemliggende instanser. Statsfrie funktioner kræver ekstern tilstandslagring og tildeling GC Generer funktioner uden autoboxing. Stateful funktioner nul allokering
Indsamling af oversigtsberegning Skub resultater fra partitionstråde til køen. Kræver tildeling og synkronisering GC / Lås Behandling af enkelt tråd, ingen tildeling eller låse

Løsninger til affaldsreduktion

Koden, der implementerer hændelsesbehandlingen, genereres ved hjælp af Fluxtion. Generering af en løsning giver mulighed for en nulomkostningsabstraktionstilgang, hvor den kompilerede løsning har et minimum af overhead. Programmøren beskriver den ønskede adfærd og på byggetidspunktet genereres en optimeret løsning, der opfylder kravene. For dette eksempel kan den genererede kode ses her.

Maven pom'en indeholder en profil til genopbygning af de genererede filer ved hjælp af Fluxtion maven-plugin'et udført med følgende kommando:

mvn -Pfluxtion install

Fillæsning

Data udtrækkes fra inputfilen som en serie CharEvents og publiceres til csv-typen marshaller. Hvert tegn læses individuelt fra filen og skubbes ind i en CharEvent. Da den samme CharEvent-instans genbruges, tildeles der ingen hukommelse efter initialisering. Logikken for streaming af CharEvents er placeret i CharStreamer-klassen. Hele filen på 96 MB kan læses med næsten nul hukommelse, der er allokeret på heapen af ​​applikationen.

CSV-behandling

Tilføjelse af en @CsvMarshaller til en javabean giver Fluxtion besked om at generere en csv-parser på byggetidspunktet. Fluxtion scanner applikationsklasser for @CsvMarshaller-annotationen og genererer marshallere som en del af byggeprocessen. For et eksempel se AssetPrice.java, som resulterer i genereringen af ​​AssetPriceCsvDecoder0. Dekoderen behandler CharEvents og samler rækkedataene til en målinstans.

De genererede CSV-parsere anvender de strategier, der er skitseret i tabellen ovenfor, og undgår unødvendig hukommelsesallokering og genbruger objektforekomster for hver behandlet række:

  • En enkelt genbrugelig forekomst af en tegnbuffere gemmer rækketegnene
  • En instans, der kan genanvendes med flyvevægt, er målet for marshalled kolonnedata
  • Konverteringer udføres direkte fra en CharSequence til måltyper uden oprettelse af mellemliggende objekter.
  • Hvis CharSequence'er bruges i målforekomsten, oprettes der ingen strenge, en flyweight Charsequence bruges.

Se upateTarget()-metoden i en AssetPriceCsvDecoder for et eksempel på affaldsfri char-til-målfeltkonvertering:

Beregninger

Denne builder beskriver aktivberegningen ved hjælp af Fluxtion-streaming-api'et. Den deklarative form ligner Java-stream-api'et, men bygger hændelsesbehandlingsgrafer i realtid. Metoder markeret med anmærkningen
@SepBuilder påkaldes af maven-plugin'et for at generere en statisk hændelsesprocessor. Koden nedenfor beskriver beregningerne for et aktiv, se
FluxtionBuilder:

@SepBuilder(name = "SymbolTradeMonitor",
            packageName = "com.fluxtion.examples.tradingmonitor.generated.symbol",
            outputDir = "src/main/java",
            cleanOutputDir = true
    )
    public void buildAssetAnalyser(SEPConfig cfg) {
        //entry points subsrcibe to events
        Wrapper<Deal> deals = select(Deal.class);
        Wrapper<AssetPrice> prices = select(AssetPrice.class);
        //result collector, and republish as an event source
        AssetTradePos results = cfg.addPublicNode(new AssetTradePos(), "assetTradePos");
        eventSource(results);
        //calculate derived values
        Wrapper<Number> cashPosition = deals
                .map(multiply(), Deal::getSize, Deal::getPrice)
                .map(multiply(), -1)
                .map(cumSum());
        Wrapper<Number> pos = deals.map(cumSum(), Deal::getSize);
        Wrapper<Number> mtm = pos.map(multiply(), arg(prices, AssetPrice::getPrice));
        Wrapper<Number> pnl = add(mtm, cashPosition);
        //collect into results
        cashPosition.push(results::setCashPos);
        pos.push(results::setAssetPos);
        mtm.push(results::setMtm);
        pnl.push(results::setPnl);
        deals.map(count()).push(results::setDealsProcessed);
        prices.map(count()).push(results::setPricesProcessed);
        //add some rules - only fires on first breach
        pnl.filter(lt(-200))
                .notifyOnChange(true)
                .map(count())
                .push(results::setPnlBreaches);
        pos.filter(outsideBand(-200, 200))
                .notifyOnChange(true)
                .map(count())
                .push(results::setPositionBreaches);
        //human readable names to nodes in generated code - not required 
        deals.id("deals");
        prices.id("prices");
        cashPosition.id("cashPos");
        pos.id("assetPos");
        mtm.id("mtm");
        pnl.id("pnl");
    }

Funktionsbeskrivelsen konverteres til en effektiv imperativ form for udførelse. En genereret hændelsesprocessor, SymbolTradeMonitor er indgangspunktet for AssetPrice- og Deal-begivenheder. Genererede hjælperklasser bruges af hændelsesprocessoren til at beregne aggregaterne, hjælperklasserne er her.

Processoren modtager hændelser fra partitioneren og påkalder hjælpefunktioner for at udtrække data og kalde beregningsfunktioner, og lagre aggregerede resultater i noder. Samlede værdier skubbes ind i felter i resultatforekomsten, AssetTradePos. Der oprettes ingen mellemobjekter, enhver primitiv beregning håndteres uden auto-boksning. Beregningsknudepunkter refererer til data fra overordnede instanser, ingen dataobjekter flyttes rundt i grafen under udførelsen. Når først grafen er initialiseret, er der ingen hukommelsestildelinger, når en hændelse behandles.

Et billede, der repræsenterer behandlingsgrafen til en aktivberegning, genereres samtidig med koden, som ses nedenfor:

Et lignende sæt af beregninger er beskrevet for porteføljen i FluxtionBuilderbuilder-klassen buildPortfolioAnalyser-metoden, der genererer en PortfolioTradeMonitor-hændelseshandler. AssetTradePos udgives fra en SymbolTradeMonitor til PortfolioTradeMonitor. De genererede filer til porteføljeberegningerne er placeret her.

Opdeling og indsamling

Alle beregninger, partitionering og samling sker i den samme enkelt tråd, ingen låse er påkrævet. Uforanderlige objekter er ikke påkrævet, da der ikke er nogen samtidighedsproblemer at håndtere. De organiserede hændelser har et isoleret privat omfang, der tillader sikker genbrug af instanser, da de genererede hændelsesprocessorer kontrollerer instansernes livscyklus under hændelsesbehandling.

Systemdataflow

Diagrammet nedenfor viser det komplette dataflow for systemet fra bytes på en disk til den offentliggjorte oversigtsrapport. De lilla kasser genereres som en del af buildet, blå kasser er genbrugelige klasser.

Konklusion

I denne artikel har jeg vist, at det er muligt at løse et komplekst hændelseshåndteringsproblem i java næsten uden spild. Funktioner på højt niveau blev brugt i en deklarativ/funktionel tilgang til at beskrive ønsket adfærd, og de genererede hændelsesprocessorer opfylder kravene i beskrivelsen. En simpel annotation udløste marshaller-generering. Den genererede kode er simpel imperativ kode, som JIT let kan optimere. Der foretages ingen unødvendige hukommelsestildelinger, og instanser genbruges så meget som muligt.

Ved at følge denne tilgang er højtydende løsninger med lavt ressourceforbrug inden for rækkevidde af den gennemsnitlige programmør. Traditionelt kunne kun specialingeniører med mange års erfaring opnå disse resultater.

Selvom den er ny i Java, er denne tilgang velkendt på andre sprog, almindeligvis kendt som nulomkostningsabstraktion.

Med nutidens cloud-baserede computermiljøer opkræves ressourcer pr. forbrugt enhed. Enhver løsning, der sparer energi, vil også have en positiv fordel på virksomhedens bundlinje.

Java tag