Latens

Hvad er latens?

Antag at du har et program som hedder “musepling” (som skal afgive en “pling”-lyd hvis du klikker på en museknap). Latensen er tiden mellem din finger trykker på museknappen til du hører plinget. Latensen for dette scenario består af flere forskellige latenstider, som har forskellige årsager.

Latens i enkle programmer

I dette enkle program opstår latensen på følgende steder:

  • Tiden til kernen har meddelt X11-serveren at museknappen er trykket ned.

  • Tiden til X11-serveren har meddelt dit program at museknappen er trykket ned.

  • Tiden til musepling-programmet har bestemt at denne knap var værd at få et pling afspillet.

  • Tiden det tager for musepling-programmet at fortælle lydserveren at den skal afspille et pling.

  • Tiden det tager for plinget (som lydserveren begynder at mikse med øvrig uddata med det samme) at gå gennem bufferet data, til det virkelig når stedet hvor lydkortet spiller.

  • Tiden det tager for pling-lyden at gå fra højtalerne til dine ører.

De første tre punkter er latensen udenfor aRts. De er interessante, men udenfor dette dokuments rækkevidde. Ikke desto mindre skal du vide at de findes, så selvom du har optimeret alt andet til virkelig små værdier, så får du måske ikke nødvendigvis nøjagtigt det resultat du forventer dig.

At bede serveren om at spille noget indebærer oftest kun et enkelt MCOP-kald. Der er målinger som bekræfter at det kan lade sig gøre at bede serveren at spille noget 9000 gange pr sekund med den nuværende implementering, på samme værtsmaskine med Unix domæne-sokler. Jeg antager at det meste af dette er kernens omkostning, for at skifte fra en proces til en anden. Naturligvis ændres denne værdi med den nøjagtige type af parametrene. Hvis man overfører et helt billede med et kald, bliver det langsommere end hvis man kun overfører en "long" værdi. Det samme er sandt for returværdien. For almindelige strenge (som filnavnet på wav-filen som skal afspilles) skulle dette ikke være et problem.

Dette betyder at vi kan tilnærme denne tid med 1/9000 sekund, det vil sige under 0,15 ms. Vi vil se at dette ikke er relevant.

Derefter kommer tiden efter serveren begynder at spille og lydkortet tager imod noget. Serveren skal buffre data, så ingen pauser høres når andre programmer, såsom din X11-server eller “musepling”-programmet, kører. Den måde dette håndteres på Linux® er at der er et antal fragmenter af en vis størrelse. Serveren genopfylder fragmenter, og lydkortet afspiller fragmenter.

Så antag at der er tre fragmenter. Serveren genopfylder det første og lydkortet begynder at afspille det. Serveren genopfylder det andet. Serveren genopfylder det tredje. Serveren er klar og andre programmer kan nu gøre noget.

Når lydkortet har afspillet det første fragment, begynder det at afspille det andet og serveren begynder at genopfylde det første, og så videre.

Den maksimale latenstiden du får med alt dette er (antal fragmenter) * (størrelse på hvert fragment) / (samplingsfrekvens * (størrelse på hver sampling)). Hvis vi antager 44 kHz stereo, og syv fragmenter på 1024 byte (de nuværende standardindstillinger i aRts), så får vi 40 ms.

Disse værdier kan indstilles efter dine behov. CPU-brugen øges dog med mindre latenstider, eftersom lydserveren skal genopfylde bufferne oftere, og med mindre dele. Det er også oftest umuligt at nå bedre værdier uden at give lydserveren realtidsprioritet, eftersom man ellers ofte får pauser.

Det er imidlertid realistisk at lave noget i stil med 3 fragmenter med 256 byte hver, som ville ændre denne værdi til 4,4 ms. Med 4,4 ms forsinkelse ville aRts CPU-forbrug være cirka 7,5 %. Med en 40 ms forsinkelse, ville den være cirka 3 % (for en PII-350, og værdien kan afhænge af dit lydkort, version af kernen og andet).

Så er der tiden som det tager for pling-lyden at gå fra højtalerne til dine ører. Antag at din afstand fra højtalerne er 2 meter. Lyden bevæger sig med hastigheden 330 meter pr sekund. Så vi kan ansætte denne tid til 6 ms.

Latenstid i programmer med lydstrømme

Programmer med lydstrømme er dem som laver deres lyd selv. Tænk dig til et spil som sender en konstant strøm med samplinger, og nu skal tilpasses til at afspille lyd via aRts. Som et eksempel: når jeg trykker på en tast så hopper figuren som jeg bruger, og en bang-lyd afspilles.

Først så skal du vide hvordan aRts håndterer strømme. Det er meget lignende I/O med lydkortet. Spillet sender nogle pakker med samplinger til lydserveren, lad os antage tre styk. Så snart lydserveren er klar med den første pakke, sender den en bekræftelse tilbage til spillet om at denne pakke er færdig.

Spillet laver yderligere en lydpakke og sender den til serveren. I mellemtiden begynder serveren at konsumere den anden lydpakke, og så videre. Latenstiderne ligner dem i det enklere tilfælde:

  • Tiden til kernen har meddelt X11-serveren at en knap er trykket ned.

  • Tiden til X11-serveren har meddelt spillet at en knap er trykket ned.

  • Tiden til spillet har bestemt at denne knap var værd at få et bang afspillet.

  • Tiden til lydpakken som afspilles er begyndt at putte bang-lyden ind når lydserveren.

  • Tiden det tager for banget (som lydserveren begynder at mikse med øvrig uddata med det samme) at gå gennem bufferdata, til det virkelig når stedet hvor lydkortet spiller.

  • Tiden det tager for bang-lyden fra højtalerne at nå dine ører.

De eksterne latenstider er, som ovenfor, udenfor dette dokuments rækkevidde.

Det er åbenbart at latenstiden for strømme afhænger af tiden det tager for alle pakker som bruges at afspilles en gang. Så den er (antal pakker) * (størrelse på hver pakke) / (samplingsfrekvensen * (størrelse på hver sampling)).

Som du ser er dette samme formel som gælder for fragmenterne. For spil er der dog ingen grund til at have så korte forsinkelser som ovenfor. Jeg vil sige at et realistisk eksempel for et spil kunne være 2048 byte pr pakke, når der bruges 3 pakker. Latenstidsresultatet ville så være 35 ms.

Dette er baseret på følgende: antag at et spil viser 25 billeder pr sekund (for skærmen). Det er antageligt helt sikkert at antage at en forskel på et billede for lydudskriften ikke vil kunne mærkes. Derfor er 1/25 sekunds forsinkelse for lydstrømmen acceptabelt, hvilket på sin side betyder at 40 ms skulle være o.k.

De fleste personer kører heller ikke deres spil med realtidsprioritet, og faren for pauser i lyden kan ikke negligeres. Strømme med 3 pakker på 256 byte er mulige (jeg prøvede det) - men forårsager meget CPU-forbrug til strømning.

Latenstider på serversiden kan du beregne præcis som ovenfor.

Nogle CPU-forbrugshensyn

Der er mange faktorer som påvirker CPU-forbrug i et komplekst scenario, med nogle programmer med lydstrømme og nogle andre programmer, nogle plugin i serveren, osv. For at angive nogle få:

  • CPU-forbrug for de nødvendige beregninger.

  • aRts interne skemalægningsomkostning - hvordan aRts bestemmer hvornår hvilket modul skal beregne hvad.

  • Omkostning til konvertering af heltal til decimaltal.

  • MCOP protokolomkostning.

  • Kernens proces/sammenhængsskift.

  • Kernens kommunikationsomkostning.

For beregning af rå CPU-forbrug, hvis du afspiller to strømme samtidigt skal du gøre additioner. Hvis du anvender et filter, er visse beregninger indblandede. For at tage et forenklet eksempel, at addere to strømme kræver måske fire CPU-cykler pr addition, på en 350 MHz processor er dette 44100 * 2 * 4 / 350000000 = 0,1 % CPU-forbrug.

aRts interne skemalægning: aRts skal bestemme hvilket plugin som skal beregne hvad hvornår. Dette tager tid. Brug et profileringsværktøj hvis du er interesseret i det. Hvad som kan siges i almindelighed er at jo mindre realtid som bruges (dvs. jo større blokke som kan beregnes af gangen) desto mindre skemalægningsomkostning fås. Udover beregning af blokke med 128 samplinger af gangen (altså med brug af fragmentstørrelser på 512 byte) er skemalægningsomkostningen formodentlig ikke værd at bryde sig om.

Konvertering fra heltal til decimaltal: aRts bruger decimaltal som internt dataformat. De er enkle at håndtere, og på moderne processorer er de ikke meget langsommere end heltalsoperationer. Hvis der er klienter som afspiller data som ikke er decimaltal (såsom et spil som skal lave sin lydudskrift via aRts), behøves konvertering. Det samme gælder hvis du vil afspille lyd på dit lydkort. Lydkortet behøver heltal, så du skal konvertere.

Her er værdier for en Celeron, cirka klokcykler pr sampling, med -O2 og egcs 2.91.66 (målt af Eugene Smith ). Dette er naturligvis yderst processorafhængigt:

convert_mono_8_float: 14
convert_stereo_i8_2float: 28
convert_mono_16le_float: 40
interpolate_mono_16le_float: 200
convert_stereo_i16le_2float: 80
convert_mono_float_16le: 80

Så dette betyder 1 % CPU-forbrug for konvertering og 5 % for interpolation på denne 350 MHz processor.

MCOP protokollomkostning: MCOP klarer, som en tommelfingerregel, 9000 kald pr sekund. En stor del af dette er ikke MCOP's fejl, men hører sammen med de to grunde for kernen som nævnes nedenfor. Dette giver i alle tilfælde en basis for at udføre beregninger af hvad omkostningen er for strømning.

Hver datapakke som sendes med en strøm kan anses for at være et MCOP-kald. Store pakker er naturligvis langsommere end 9000 pakker/s, men det giver en god idé.

Antag at du bruger pakkestørrelser på 1024 byte. På denne måde, for at overføre en strøm med 44 kHz stereo, behøver du at overføre 44100 * 4 / 1024 = 172 pakker pr sekund. Antag at du kunne overføre 9000 pakker med 100 % CPU-forbrug, så får du (172 *100) / 9000 = 2 % CPU-forbrug på grund af strømningen med 1024 byte pakker.

Dette er en approksimation. Det viser i alle tilfælde at du ville klare dig meget bedre (hvis du har råd for latenstiden), med for eksempel at bruge pakker på 4096 byte. Her kan vi oprette en kompakt formel, ved at beregne pakkestørrelsen som forårsager 100 % CPU-forbrug som 44100 * 4 / 9000 = 19,6 samplinger, og på den måde få hurtigformlen:

CPU-forbrug for en strøm i procent = 1960 / (din pakkestørrelse)

som giver os 0,5 % CPU-forbrug med en strøm af 4096 byte pakker.

Kernens proces/sammenhængsskift: Dette er en del af MCOP-protokollens omkostning. At skifte mellem to processer tager tid. Der er en ny hukommelsesafbildning, cacher er ugyldige, og en del andet (hvis en ekspert på kernen læser dette - fortæl mig om de nøjagtige grunde). Dette betyder: det tager tid.

Jeg er ikke sikker på hvor mange procesbyte Linux® kan lave pr sekund, men værdien er ikke uendelig. Så af MCOP-protokollens omkostning, antager jeg at en hel del afhænger af processkift. MCOP først påbegyndtes prøvede jeg samme kommunikation inde i en proces, og det var meget hurtigere (cirka fire gange hurtigere).

Kernens kommunikationsomkostning: Dette er en del af MCOP-protokollens omkostning. At overføre data mellem processer gøres for øjeblikket via et udtag (sokkel). Dette er bekvemt, eftersom den almindelige select() metode kan bruges til at afgøre hvornår en meddelelse er ankommet. Det kan også kombineres med andre I/O-kilder såsom lyd-I/O, X11-server eller hvad som helst andet.

Disse læse- og skrivekald koster definitivt processorcykler. For små kald (som at overføre en midi-begivenhed) er det formodentlig ikke så farligt, men for store kald (som at overføre et videobillede på flere Mbyte) er det helt klart et problem.

At tilføje brug af delt hukommelse til MCOP hvor det er passende er formodentlig den bedste løsning. Det skal dog gøres transparent for anvendelsesprogrammerne.

Tag et profileringsværktøj og udfør andre test for at finde ud af nøjagtigt hvordan nuværende lydstrømme påvirkes af ikke at bruge delt hukommelse. Det er dog ikke så dårligt, eftersom lydstrømme (afspilning af mp3) kan gøres med totalt 6 % CPU-forbrug for artsd og artscat (og 5 % for mp3-afkoderen). Dette omfatter alting fra nødvendige beregninger til omkostning for udtaget, så jeg vil bedømme at man måske ville vinde cirka 1 % på at bruge delt hukommelse.

Nogle hårde værdier

Disse er lavet med den nuværende udviklingsversion. Jeg ville også forsøge med rigtigt svære tilfælde, så dette er ikke hvad programmer til daglig brug ville gøre.

Jeg skrev et program som hedder streamsound som sender datastrømmen til aRts. Her køres det med realtidsprioritet (uden problemer), og et lille plugin på serversiden (lydstyrkeskalning og klipning):

4974 stefan    20   0  2360 2360  1784 S       0 17.7  1.8   0:21 artsd
 5016 stefan    20   0  2208 2208  1684 S       0  7.2  1.7   0:02 streamsound
 5002 stefan    20   0  2208 2208  1684 S       0  6.8  1.7   0:07 streamsound
 4997 stefan    20   0  2208 2208  1684 S       0  6.6  1.7   0:07 streamsound

Hvert af programmerne sender en strøm med 3 fragmenter på 1024 byte (18 ms). Der er tre sådanne klienter som kører samtidigt. Jeg ved at det synes at være lidt vel meget, men som jeg sagde: tag et profileringsværktøj og find ud af hvad som koster tid, og hvis du vil, forbedr det.

Jeg tror i alt fald ikke at bruge strømning sådan her er realistisk eller giver mening. For at gøre det hele endnu mere ekstremt, forsøgte jeg med den mindst mulige latenstid. Resultat: man kan bruge strømme uden afbrud med et klientprogram, hvis man tager 2 fragmenter med 128 byte mellem aRts og lydkortet, og mellem klientprogrammet og aRts. Dette betyder at man har en total maksimal latenstid på 128 * 4 / 44100 * 4 = 3 ms, hvor 1,5 ms genereres på grund af I/O til lydkortet og 1,5 ms genereres af kommunikation med aRts. Begge programmer skal køre med realtidsprioritet.

Men dette koster en enorm mængde CPU. Dette eksempel koster cirka 45 % på min P-II/350. Det begynder også at klikke hvis man starter top, flytter vinduer på X11-skærmen eller laver disk-I/O. Alt dette har med kernen at gøre. Problemet er at skemalægge to eller flere processer med realtidsprioritet også koster en enorm anstrengelse, endnu mere end kommunikation og meddelelse til hinanden, osv..

Tilsidst, et mere hverdagsagtigt eksempel: Dette er aRts med artsd og en artscat (en klient med datastrøm) som kører 16 fragmenter på 4096 byte:

5548 stefan    12   0  2364 2364  1752 R       0  4.9  1.8   0:03 artsd
 5554 stefan     3   0   752  752   572 R       0  0.7  0.5   0:00 top
 5550 stefan     2   0  2280 2280  1696 S       0  0.5  1.7   0:00 artscat