Search code examples
vb.netmemory-leaksdisposecircular-buffer

High Speed Circular Buffer


I have been asked to improve the memory efficiency of an application that is used for high speed data acquisition. After running the VS memory profiler several times and scanning the projects for obvious issues I came to the following conclusion: despite using a fixed size circular buffer for storing acquired sample points, the RAM usage increases relative to the polling period. For example: polling data for one signal at 2 microseconds can use 5 times more memory (private bytes) than when running at 50 microseconds ... even though the buffer size is the same.

The circular buffer is an array of SamplePoint objects. Each of these objects contains an array of Shorts (16 bit) for corresponding data per signal and one date object (8 bytes) for the time stamp. For efficiency, the circular buffer is Redimmed only once every time it begins polling and filled with empty sample points which are then "assigned".

Also, it seems that when we stop and run the application it hogs even more memory each time as though the Redim is not releasing the previous array.

My questions boil down to the following:

What is most memory efficient way of implementing a circular buffer containing managed objects which have arrays? Also, how and why is memory usage increasing with a fixed size array with different polling speeds? Does the garbage collector not have time? Do local variables get disposed of IMMEDIATELY when a sub or function exits?

These are some of the doubts and concerns that I would like to rule out before proceeding. Thank you for taking the time. Also, I could post code but it would be pointless as there is a lot of it and spread out.

Edit: Here is some condensed code I wrote that reflects the initialization, population and reset of the circular buffer. See any bugs?

    ''' <summary>
''' Initialize internal list circular buffer.
''' </summary>
''' <param name="sizeOfBuffer"></param>
''' <remarks>
''' This is done for efficiency to avoid creating new samples points 
''' and redimensioning samplepoint data arrays for every read. Instead
''' the buffer is created and each samplepoint re-used. 
''' </remarks>
Friend Sub InitializeCircularBuffer(ByVal sizeOfBuffer As Integer, ByVal smpleDataSize As Integer, ByVal name As String)

    Dim mutexName As String = CreateMutexName(name)
    'First check for already existing mutex, otherwise create a new one
    Try
        _Mutex = Mutex.OpenExisting(mutexName)
    Catch ex As WaitHandleCannotBeOpenedException
        'Intialize mutex for each shared memory with unique names
        _Mutex = New Mutex(False, mutexName)
    Catch ex As UnauthorizedAccessException
        'Intialize mutex for each shared memory with unique names
        _Mutex = New Mutex(False, mutexName)
    End Try

    _Mutex.WaitOne()
    Try
        _SampleDataSize = smpleDataSize

        'Check size is valid, otherwise use the shared memory numSamples as default
        If sizeOfBuffer <= 0 Then
            _CircularBufferSize = _DefaultBufferSize
        Else
            _CircularBufferSize = sizeOfBuffer
        End If

        'Initialize/Reset circular buffer
        If _CircularBuffer Is Nothing Then
            _CircularBuffer = New List(Of SHM_SamplePoint)
        Else
            _CircularBuffer.Clear()
        End If

        'Create empty sample points with redimensioned data arrays in buffer 
        For i = 0 To _CircularBufferSize - 1
            _CircularBuffer.Add(New SHM_SamplePoint(_SampleDataSize))
        Next

        'Set current index to last place in buffer
        'It is incremented to first place when buffer
        'is being populated
        _CurrentIndex = _CircularBufferSize - 1

        _CircularBufferInitialized = True
    Catch ex As Exception
    Finally
        _Mutex.ReleaseMutex()
    End Try
End Sub

''' <summary>
''' Packages raw data and populates circular buffer.
''' </summary>
Friend Sub PopulateCircularBuffer(ByRef rawData() As Double, ByVal rawTimeStamps() As Double, ByVal numSamples As Integer, Optional ByVal startIndex As Integer = 0)
    _Mutex.WaitOne()
    Try
        _NumNewSamples = numSamples
        If _NumNewSamples > 0 Then
            For i As Integer = startIndex To _NumNewSamples - 1
                'Get index of next sample to be overwritten
                _CurrentIndex = (_CurrentIndex + 1) Mod _CircularBufferSize
                'Assign time-stamp
                _CircularBuffer(_CurrentIndex).TimeStamp = Date.FromOADate(rawTimeStamps(i))
                'Assign data
                Array.ConstrainedCopy(rawData, (i * _SampleDataSize), _CircularBuffer(_CurrentIndex).Data, 0, _SampleDataSize)
            Next
        End If
    Catch ex As Exception
    Finally
        _Mutex.ReleaseMutex()
    End Try
End Sub

''' <summary>
''' Empty the circular buffer.
''' </summary>
''' <remarks></remarks>
Friend Sub ResetCircularBuffer()
    For i As Integer = 0 To _CircularBuffer.Count - 1
        _CircularBuffer(i).Data = Nothing
        _CircularBuffer(i).TimeStamp = Nothing
    Next
    _CircularBuffer.Clear()
    _CircularBuffer.TrimExcess()
    'Signal garbage collection
    GC.Collect()
    _CircularBufferSize = 0
    _CircularBufferInitialized = False
End Sub

Solution

  • The Garbage Collector will dispose of objects when it feels it's a good time to do so. In fact, one app that I am working on now acquires memory so fast that the GC does not begin releasing RAM until the process is using around 1.4GB of RAM (even though only about 100K is actively in use, the rest is eligible for collection).

    It's not at all clear to me based on your description why memory utilization should be inversely proportional to sample rate (given a fixed-sized buffer). I agree with the comment that it would be wise to post the relevant code.

    You might try using the Server Garbage Collector if you're currently using the workstation one (or vice versa) to see if your results vary.

    Should we use "workstation" garbage collection or "server" garbage collection?