[英]Efficient circular buffer in C++ which will be passed to C-style array function parameter
我正在尋求有關我解決以下問題的方法的建議。 我有一個恆定的數據輸入,需要添加到緩沖區中,並且在每次迭代時,我需要將緩沖的數據傳遞給 function,該 function 通過指針接受 C 樣式數組。
我擔心效率,所以我思考如何在某種循環緩沖區中存儲和管理數據,同時將其作為順序原始數據傳遞給所述 function。
我目前的方法可以總結為以下示例:
#include <iostream>
#include <array>
#include <algorithm>
void foo(double* arr, int size)
{
for (uint k = 0; k < size; k++)
std::cout << arr[k] << ", ";
std::cout << std::endl;
}
int main()
{
const int size = 20;
std::array<double, size> buffer{};
for (double data = 0.0; data < 50.0; data += 1.0)
{
std::move(std::next(std::begin(buffer)), std::end(buffer), std::begin(buffer));
buffer.back() = data;
foo(buffer.data(), size);
}
}
在實際用例中,緩沖區還需要在開始時填充到“const”數據大小(我在這里使用引號是因為大小可能在編譯時或可能不知道,但一旦知道,它永遠不會改變)。
我將數據存儲在std::array
中(如果在編譯時不知道大小,則存儲在std::vector
中),因為數據在 memory 中是連續的。 當我需要插入新數據時,我使用 forward std::move
移動所有內容,然后手動替換最后一項。 最后,我只是將std::array::data()
及其大小傳遞給 function。
雖然乍一看這應該有效,但原因告訴我,因為數據是按順序存儲的,整個緩沖區仍將使用std::move
復制,並且每次插入都是 O(n)
實際緩沖區大小可能只有數百個,並且數據最大達到 100Hz,但問題是我需要盡快調用 function 的結果,所以我不想在緩沖區管理上浪費時間(即使我們說的很少,甚至不到毫秒)。 我對此有很多疑問,但他們的候選名單如下:
謝謝你的回答維爾納。 當我在 Repl.it 上運行此解決方案時,我得到:
it took an average of 21us and a max of 57382us
為了比較,我最初的想法與相同的緩沖區大小有以下結果:
it took an average of 19us and a max of 54129us
這意味着我最初的方法確實很幼稚:)
與此同時,在等待答案的同時,我想出了以下解決方案:
#include <iostream>
#include <array>
#include <algorithm>
#include <chrono>
void foo(double* arr, int size)
{
for (uint k = 0; k < size; k++)
std::cout << arr[k] << ", ";
std::cout << std::endl;
}
int main()
{
const int buffer_size = 20;
std::array<double, buffer_size*2> buffer{};
int buffer_idx = buffer_size;
for (double data = 0.0; data < 100.0; data += 1.0)
{
buffer.at(buffer_idx - buffer_size) = data;
buffer.at(buffer_idx++) = data;
foo(buffer.data() + buffer_idx - buffer_size, buffer_size);
buffer_idx -= buffer_size * (buffer_idx == buffer_size * 2);
}
}
由於緩沖區的大小不是問題,我分配了兩倍所需的 memory 並在兩個位置插入數據,偏移緩沖區大小。 當我到達終點時,我就像打字機一樣回來了。 這個想法是我通過存儲一個數據副本來偽造循環緩沖區,這樣它就可以讀取數據,就好像它穿過了整個圓圈一樣。
對於 50000 的緩沖區大小,這給了我以下結果,這正是我想要的:
it took an average of 0us and a max of 23us
您將始終必須復制數據,因為不存在“連續”環形緩沖區(也許在某些花哨的硅片中確實存在)。
您也不能初始化運行時定義大小的數組模板。
您可以使用向量來實現此目的:
#include <iostream>
#include <chrono>
#include <deque>
#include <vector>
int main() {
std::vector<double> v;
// pre fill it a little
for(double data = 0.0; data > -50000.0; data -= 1.0) {
v.push_back(data);
}
size_t cnt = 0;
int duration = 0;
int max = 0;
for(double data = 0.0; data < 50000.0; data += 1.0, ++cnt) {
auto t1 = std::chrono::high_resolution_clock::now();
v.push_back(data);
v.erase(v.begin());
// foo(v.data(), v.size());
auto t2 = std::chrono::high_resolution_clock::now();
auto delta = std::chrono::duration_cast<std::chrono::microseconds>( t2 - t1 ).count();
duration += delta;
if(max == 0 || max < delta) {
max = delta;
}
}
std::cout << "it took an average of " << duration / cnt << "us and a max of " << max << " us" << std::endl;
return 0;
}
Output:
it took an average of 11us and a max of 245 us
除了 stribor14的回答之外,我還有另外兩個建議。 這些僅基於性能,因此此處不會真正找到可讀或可維護的代碼。
我在閱讀問題時的第一個想法也是分配兩倍的存儲量,但只寫一次。 寫完所有位置后,后半部分將被復制到前半部分。 我的第一直覺說這可能是一個更好的表現。 我的理由是,將發生相同數量的總寫入,但所有寫入都是順序的(而不是每秒跳一次寫入到數組中的另一個位置)。
#include <cstddef>
#include <cstring>
#include <array>
const size_t buffer_size = 50'000;
int main()
{
std::array<double, 2 * buffer_size> buffer{};
double *index = buffer.data();
double *mid = index + buffer_size;
for (double data = 0.0; data < 10 * buffer_size; data += 1.0)
{
if (index == mid)
{
index = buffer.data();
std::memcpy(index, mid, buffer_size * sizeof(double));
}
*(index++ + buffer_size) = data;
foo(index, buffer_size);
}
}
或者,我認為可以優化 OP 自己的答案以刪除數組訪問。 這個想法是buffer[buffer_idx - buffer_size]
需要 2 個加法來計算該值的位置,即: *(buffer + buffer_idx - buffer_size)
。 如果buffer_idx
包含一個指針,則只需要添加一個。 這給出了以下代碼:
#include <cstddef>
#include <array>
const size_t buffer_size = 50'000;
int main()
{
std::array<double, buffer_size * 2> buffer{};
double *index = buffer.data();
double *mid = buffer.data() + buffer_size;
for (double data = 0.0; data < 10 * buffer_size; data += 1.0)
{
*index = data;
*(index + buffer_size) = data;
++index;
index -= buffer_size * (index == mid);
foo(index, buffer_size);
}
}
現在我注意到我正在走下 C++ 優化的兔子洞。 所以我們不能止步於此。 為了選擇使用哪個實現,我想運行一個基准測試。 Werner Pirkl 給出了一個很好的起點。 但是在我們優化的代碼上運行它是沒有意義的,因為測量的時間是 0μs。 所以讓我們稍微改變一下我在基准測試中寫了一個循環來給它一些運行時間並想出了:
const int repeats = 1000;
volatile double *ptr;
int duration = 0;
const size_t buffer_size = 50'000;
// ... Set up of the buffers and indices
for (int i = 0; i < repeats; ++i)
{
auto t1 = std::chrono::high_resolution_clock::now();
for (double data = 0.0; data < 10 * buffer_size; data += 1.0)
{
// ... add data to circular buffer
ptr = // ... the start of the array
}
auto t2 = std::chrono::high_resolution_clock::now();
duration += std::chrono::duration_cast<std::chrono::microseconds>(t2 - t1).count();
}
(注意使用volatile double *
來確保指向連續數組的原始指針不會被優化掉。)
在運行這些測試時,我注意到它們非常依賴於編譯器標志(-O2 -O3 -march=native...)。 我將給出一些結果,但就像所有 C++ 基准一樣,對它持保留態度,並在真實世界的工作負載下運行你自己的。 (報告的時間是每次插入的平均 ns)
with `memcpy` stribor14 `operator[]` with pointers
|---------------|-----------|--------------|---------------|
-O2 | 1.38 | 1.57 | 1.41 | 1.15 |
-O3 | 1.37 | 1.63 | 1.36 | 1.09 |
-O3 -march=native | 1.35 | 1.61 | 1.34 | 1.09 |
不用說:我對我認為應該表現最好的東西感到非常失望。 但如前所述,該基准絕不代表任何現實世界的表現。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.