Ik ben al even bezig met een multi-threaded programma waarbij ik beeld-data en bijbehorende metadata per frame tussen 2 threads wil kunnen bufferen.
Dus ik heb een BufferQueue object gemaakt.
Omdat de hoeveelheid data per frame kan verschillen, moet er een write-request gedaan worden waarbij een pointer wordt opgeleverd (die dus NULL kan zijn) waar begonnen kan worden met schrijven en na afloop een writeCommit met de bijbehorende metadata en hoeveel data er werkelijk is geschreven.
Bij het request wordt dan aangegeven wat de geschatte bovengrens is van de hoeveelheid data die je wilt gaan schrijven.
N.B. er is dus 1 producer en 1 consumer.
Om dit op te lossen heeft het BufferQueue object een (hele) grote aaneengesloten buffer voor de pixel-data.
De informatie waar voor elk frame de data begint, hoeveel data erin zit en de metadata, wordt opgeslagen in een BufferInfo object.
Zo'n BufferInfo-object wordt weer opgeslagen in een CircularBuffer object.
Dat CircularBuffer-object bestaat uit een std::vector die 3 indices bij moet houden:
- readIndex
- writeIndex
- lastWrittenIndex.
Die lastWrittenIndex is nodig omdat de BufferQueue in 2 modi moet kunnen werken:
- processAllData
- processLastWritten
Oftewel de consumer-thread moet in bepaalde toepassingen data kunnen skippen wanneer de consumer het niet bij kan houden. Bijvoorbeeld live video moet gewoon snappy zijn en dus altijd het laatste beeld tonen, maar bij de focus kun je je niet permitteren om wat te missen, maar even wachten is dan niet erg.
Hieruit volgen de volgende definities voor isFull() en isEmpty():
De indices zijn allen gedeclareerd als volatile size_t.
Om de boel een beetje rap te houden, heb ik een aantal optimalisaties toegepast:
- Geen locking tenzij echt niet anders kan (duurt te lang)
- Data in de "pixelbuffer" is cache-aligned. Oftewel elke write-pointer begint altijd op een adres wat een veelvoud is van de grootte van een cache-line op de processor. Dit om te voorkomen dat lees- en write-thread op elkaar moeten wachten.
- de indices zijn ook op 'cache-line-size' afstand van elkaar gedeclareerd, zodat ze elkaar niet in de weg zitten.
Dit is een hele lange inleiding om het probleem te kunnen beschrijven.
Deze buffer werkt geweldig.... maar niet altijd.
Heel soms (soms pas na 100GB aan data) komt het voor dat de read-thread een pointer krijgt die wijst naar data die nog net niet overschreven is met nieuwe data.
Oftewel je loopt dan qua beelden net zolang achter als dat de buffer lang is. (voor 100 MB buffer met 40 MB/s is dat dus 2.5 sec)
Het heeft er dus alle schijn van dat de read-index niet op lastWritten wordt gezet, maar op write-index, of mogelijk zelfs nog daarvoor.
Ik heb allerlei asserts in de code gehad om dat te proberen te detecteren, maar tot op heden niets kunnen vinden wat dat aantoont. Telkens als ik de debugger inspecteer, staan er precies de waarden die je zou verwachten.
En nu komt het gekke, die vertraging blijft constant. Je zou verwachten dat wanneer de readpointer op writepointer komt te staan, dat de buffer dan vol is, dus dat er geen data bij komt, dus dat de reader vanzelf de buffer verder leeg moet lezen. De reader zou het makkelijk moeten kunnen bijbenen (CPU-load is laag genoeg)
De vertraging kan makkelijk uren aan een stuk zo blijven, tot de reader-thread het een keer niet meer bij kan houden en dus weer een paar frames achter komt te liggen. Dan gaat het weer gewoon verder zoals het hoort.
Het enige wat ik kan bedenken is dat de reader op een gegeven moment een BufferInfo object krijgt wat een andere inhoud heeft dan die er net in is geschreven. Oftewel dat ik naar een oude cache zit te kijken.
De std::vector waarin ik de BufferInfo objecten opsla is niet volatile gedeclareerd.
Nu kun je std-containers ook niet volatile declareren, maar het element BufferInfo wat ik erin zet is niet volatile.
Mijn vraag is, is het überhaupt mogelijk dat ik steeds tegen een verouderde set data aan zit te kijken?
Nog een paar toevoegingen:
- er is een flush-functie in BufferQueue die in CircularQueue de readIndex gelijk maakt aan lastWritten. Hiervoor heb ik de update van de readIndex in een critical section geplaatst. Die is heel snel wanneer je 'm vrijwel altijd uitvoert vanuit dezelfde thread.
- ik gebruik een core i7 (1 socket dus) en de hele code bestaat uit zo'n 12 threads (1x QT, 1x USB read-thread, 1x image processing thread, 1x display thread en 8 threads van OpenMP voor de deBayering en verdere post-processing van het beeld)
- ik heb echt performance nodig in de zin van veel frames per second, dus locking is niet een optie. Ik haal nu makkelijk 1000-en frames per sec, maar er komen ook wel situaties voor dat ik ook echt 300 fps moet verwerken. Locks kunnen zomaar 10 - 50 ms duren als het tegenzit en dan heb ik al tig frames verloren.
Dus ik heb een BufferQueue object gemaakt.
Omdat de hoeveelheid data per frame kan verschillen, moet er een write-request gedaan worden waarbij een pointer wordt opgeleverd (die dus NULL kan zijn) waar begonnen kan worden met schrijven en na afloop een writeCommit met de bijbehorende metadata en hoeveel data er werkelijk is geschreven.
Bij het request wordt dan aangegeven wat de geschatte bovengrens is van de hoeveelheid data die je wilt gaan schrijven.
N.B. er is dus 1 producer en 1 consumer.
Om dit op te lossen heeft het BufferQueue object een (hele) grote aaneengesloten buffer voor de pixel-data.
De informatie waar voor elk frame de data begint, hoeveel data erin zit en de metadata, wordt opgeslagen in een BufferInfo object.
Zo'n BufferInfo-object wordt weer opgeslagen in een CircularBuffer object.
Dat CircularBuffer-object bestaat uit een std::vector die 3 indices bij moet houden:
- readIndex
- writeIndex
- lastWrittenIndex.
Die lastWrittenIndex is nodig omdat de BufferQueue in 2 modi moet kunnen werken:
- processAllData
- processLastWritten
Oftewel de consumer-thread moet in bepaalde toepassingen data kunnen skippen wanneer de consumer het niet bij kan houden. Bijvoorbeeld live video moet gewoon snappy zijn en dus altijd het laatste beeld tonen, maar bij de focus kun je je niet permitteren om wat te missen, maar even wachten is dan niet erg.
Hieruit volgen de volgende definities voor isFull() en isEmpty():
code:
1
2
3
4
5
6
7
| bool isFull() const { return (_writeIndex == _readIndex); } bool isEmpty() const { return (_readIndex == _lastWrittenIndex); } |
De indices zijn allen gedeclareerd als volatile size_t.
Om de boel een beetje rap te houden, heb ik een aantal optimalisaties toegepast:
- Geen locking tenzij echt niet anders kan (duurt te lang)
- Data in de "pixelbuffer" is cache-aligned. Oftewel elke write-pointer begint altijd op een adres wat een veelvoud is van de grootte van een cache-line op de processor. Dit om te voorkomen dat lees- en write-thread op elkaar moeten wachten.
- de indices zijn ook op 'cache-line-size' afstand van elkaar gedeclareerd, zodat ze elkaar niet in de weg zitten.
Dit is een hele lange inleiding om het probleem te kunnen beschrijven.
Deze buffer werkt geweldig.... maar niet altijd.
Heel soms (soms pas na 100GB aan data) komt het voor dat de read-thread een pointer krijgt die wijst naar data die nog net niet overschreven is met nieuwe data.
Oftewel je loopt dan qua beelden net zolang achter als dat de buffer lang is. (voor 100 MB buffer met 40 MB/s is dat dus 2.5 sec)
Het heeft er dus alle schijn van dat de read-index niet op lastWritten wordt gezet, maar op write-index, of mogelijk zelfs nog daarvoor.
Ik heb allerlei asserts in de code gehad om dat te proberen te detecteren, maar tot op heden niets kunnen vinden wat dat aantoont. Telkens als ik de debugger inspecteer, staan er precies de waarden die je zou verwachten.
En nu komt het gekke, die vertraging blijft constant. Je zou verwachten dat wanneer de readpointer op writepointer komt te staan, dat de buffer dan vol is, dus dat er geen data bij komt, dus dat de reader vanzelf de buffer verder leeg moet lezen. De reader zou het makkelijk moeten kunnen bijbenen (CPU-load is laag genoeg)
De vertraging kan makkelijk uren aan een stuk zo blijven, tot de reader-thread het een keer niet meer bij kan houden en dus weer een paar frames achter komt te liggen. Dan gaat het weer gewoon verder zoals het hoort.
Het enige wat ik kan bedenken is dat de reader op een gegeven moment een BufferInfo object krijgt wat een andere inhoud heeft dan die er net in is geschreven. Oftewel dat ik naar een oude cache zit te kijken.
De std::vector waarin ik de BufferInfo objecten opsla is niet volatile gedeclareerd.
Nu kun je std-containers ook niet volatile declareren, maar het element BufferInfo wat ik erin zet is niet volatile.
Mijn vraag is, is het überhaupt mogelijk dat ik steeds tegen een verouderde set data aan zit te kijken?
Nog een paar toevoegingen:
- er is een flush-functie in BufferQueue die in CircularQueue de readIndex gelijk maakt aan lastWritten. Hiervoor heb ik de update van de readIndex in een critical section geplaatst. Die is heel snel wanneer je 'm vrijwel altijd uitvoert vanuit dezelfde thread.
- ik gebruik een core i7 (1 socket dus) en de hele code bestaat uit zo'n 12 threads (1x QT, 1x USB read-thread, 1x image processing thread, 1x display thread en 8 threads van OpenMP voor de deBayering en verdere post-processing van het beeld)
- ik heb echt performance nodig in de zin van veel frames per second, dus locking is niet een optie. Ik haal nu makkelijk 1000-en frames per sec, maar er komen ook wel situaties voor dat ik ook echt 300 fps moet verwerken. Locks kunnen zomaar 10 - 50 ms duren als het tegenzit en dan heb ik al tig frames verloren.
Een goedkope voeding is als een lot in de loterij, je maakt kans op een paar tientjes korting, maar meestal betaal je de hoofdprijs. mijn posts (nodig wegens nieuwe layout)