3.5.1.17.7 C++ Double Buffer and Shared Buffer

With SmartHLS's 3.5.1.11 Multi-threading with SmartHLS Threads feature, you can create a dataflow design where producer and consumer functions/threads execute simultaneously with data flowing from producer to consumer (also see 3.5.1.13 Data Flow Parallelism with SmartHLS Threads). Multi-threaded dataflow designs typically need some form of buffering and handshaking to pass intermediate data from producer thread to consumer thread. For example, SmartHLS's FIFO class allows to buffer intermediate data and the blocking write() and read() methods implement the handshaking for data passing. However, the FIFO construct is not suitable for the scenarios where the producer and consumer access the data in different orders, or require repeated accesses to the data. In this case, SmartHLS's DoubleBuffer and SharedBuffer classes can be useful alternatives.

The DoubleBuffer<T> (or SharedBuffer<T>) class contains two copies of storage (or one copy for SharedBuffer) for buffering the data of type T, which is specified as a template argument of the class. Each class object is expected to be accessed by two functions, typically two threads, one producer writing data to the buffer, and one consumer reading data from the buffer. The producer and consumer functions will use the following class methods to access the buffer and perform synchronization.

Producer Side MethodsDescription
T &producer()Returns a reference to the buffer that should be used exclusively by the producer function. The reference stays unchanged throughout the entire lifetime of the buffer object. Hence the producer function typically only needs to call this method once. Although the producer function is meant to store output to the buffer, the producer function can still read back the self-written data via the reference.
void producer_acquire()Acquires a buffer for producer to store the output. After this function returns, the producer may start writing to the buffer. This method is a blocking call – if there is an available buffer, the method returns immediately; otherwise the method blocks until a buffer becomes available, after the consumer side calls the consumer_release() method to release a buffer. Initially all buffers (2 for DoubleBuffer, 1 for SharedBuffer) are available for the producer function to acquire.
void producer_release()Releases the previously acquired buffer after finish writing output. The released buffer can then be acquired by the consumer to access as input. This method is not a blocking call and returns immediately. If the producer does not have an acquired buffer when calling this release method, the method simply returns with no operation, no buffer will be released.
Consumer Side Methods
T &consumer()Returns a reference to the buffer that should be used exclusively by the consumer function. The reference stays unchanged throughout the entire lifetime of the buffer object. Hence the consumer function typically only needs to call this method once. Although the consumer function is meant to read the buffer as input, the consumer function can also write data to the buffer via the reference. However the data written by the consumer function won’t be visible to the producer function.
void consumer_acquire()Acquires a producer-released buffer in the DoubleBuffer or SharedBuffer for the consumer to access as input. This method is a blocking call – if there is a producer-released buffer available, the method returns immediately; otherwise the method blocks until a buffer becomes available, after the producer side calls the producer_release() method to release a buffer.
void consumer_release()Releases the previously acquired buffer after finish reading input. The released buffer is returned back to the producer side for producer to write the next set of data. This method is not a blocking call and returns immediately. If the consumer does not have an acquired buffer when calling this release method, the method simply returns with no operation, no buffer will be released.

Let's use an example to illustrate the usage of these class methods. Say we have a CopyArray function that copies from array A to array B. If we want to overlap CopyArray function's execution with its upstream function (who produces A) and downstream function (who consumes B), we can invoke the upstream, CopyArray and downstream functions as threads, and change the intermediate arrays to DoubleBuffer data type to implement shared storage and synchronization between threads. The CopyArray function would be the consumer of input array A and the producer of output array B. The code below demonstrates the changes to access input and output arrays as DoubleBuffer type. To use SharedBuffer instead, the only change in the code below is to use SharedBuffer<int[100]>; all other class methods stay the same.

void CopyArray(int A[100], int B[100]) {
    for (int i = 0; i < 100; i++) B[i] = A[i];
}

//=== Change to use DoubleBuffer: ===

void CopyArray(hls::DoubleBuffer<int[100]> &BufferA, hls::DoubleBuffer<int[100]> &BufferB) {
    auto &A = BufferA.consumer(); // Obtain the reference to input buffer.
    auto &B = BufferB.producer(); // Obtain the reference to output buffer.

    BufferA.consumer_acquire();  // Acquire input buffer from upstream.
    BufferB.producer_acquire();  // Acquire buffer for output.

    // Note that the original code reading/writing the data
    // stays unchanged via the references with 'T&' type.
    for (int i = 0; i < 100; i++) B[i] = A[i];

    BufferA.consumer_release();  // Release input buffer back to upstream.
    BufferB.producer_release();  // Release output buffer to downstream.
}