WEBKT

C++20 Ranges 在嵌入式系统中的内存优化:实战技巧与案例分析

40 0 0 0

1. C++20 Ranges 简介

2. Ranges 的内存占用分析

3. 优化技巧:减少 Ranges 的内存占用

3.1. 避免不必要的拷贝

3.2. 使用 span 代替 vector

3.3. 使用自定义分配器

3.4. 减少视图的组合

3.5. 避免捕获大型对象

3.6. 使用 for_each 代替 to

3.7. 考虑手动循环

3.8. 编译优化选项

4. 案例分析

5. 总结

在资源受限的嵌入式系统中,内存管理至关重要。C++20 Ranges 库的引入,为数据处理带来了新的可能性,但同时也带来了潜在的内存开销。本文将深入探讨 C++20 Ranges 在嵌入式系统中的内存占用情况,并提供一系列实用的优化技巧,助你打造高效、低内存占用的嵌入式应用。

1. C++20 Ranges 简介

C++20 Ranges 库提供了一种新的方式来操作数据序列,它基于**视图(views)**的概念,允许你以声明式的方式组合各种数据转换和过滤操作,而无需创建中间数据结构。这种延迟计算的特性,在理论上可以减少内存占用,但实际效果取决于你的代码实现。

例如,以下代码使用 Ranges 过滤一个整数向量,并计算其平方和:

#include <iostream>
#include <vector>
#include <numeric>
#include <range/v3/all.hpp>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto even_squared_sum = numbers | ranges::views::filter([](int n){ return n % 2 == 0; })
| ranges::views::transform([](int n){ return n * n; })
| ranges::to<std::vector<int>>();
// int sum = std::accumulate(even_squared_sum.begin(), even_squared_sum.end(), 0);
for (auto i : even_squared_sum)
std::cout << i << std::endl;
// std::cout << "Sum of even squared numbers: " << sum << std::endl;
return 0;
}

这段代码首先使用 ranges::views::filter 过滤出偶数,然后使用 ranges::views::transform 计算每个偶数的平方,最后使用ranges::to<std::vector<int>>将结果存到一个新的vector中。整个过程没有创建额外的临时向量,理论上效率很高。但是真的如此吗?在嵌入式环境下,我们需要更仔细地分析。

2. Ranges 的内存占用分析

Ranges 的内存占用主要来自以下几个方面:

  • 视图对象本身: 每个视图对象(例如 filter_view, transform_view)都会占用一定的内存,虽然通常很小,但如果组合了大量的视图,累积起来也不可忽视。
  • 迭代器: Ranges 使用迭代器来遍历数据序列。不同类型的迭代器,其内存占用也不同。例如,input_iterator 通常比 random_access_iterator 占用更少的内存。
  • 闭包: 视图中使用的 lambda 表达式(闭包),可能会捕获外部变量,导致额外的内存占用。尤其是在嵌入式环境下,要避免捕获大型对象或资源。
  • 底层容器: Ranges 库通常操作的是现有的容器(例如 std::vector, std::array)。这些容器本身的内存占用,也是需要考虑的。
  • 临时对象的创建: 虽然 Ranges 提倡延迟计算,但在某些情况下,仍然会创建临时对象。例如,在使用 ranges::to 将 Range 转换为容器时,就需要分配内存来存储结果。

3. 优化技巧:减少 Ranges 的内存占用

以下是一些减少 Ranges 内存占用的实用技巧:

3.1. 避免不必要的拷贝

在 Ranges 操作中,要尽量避免不必要的拷贝。例如,如果你的数据源是一个大型的 std::vector,那么在传递给 Ranges 之前,可以使用 std::move 将其所有权转移给 Range,避免复制整个向量。

3.2. 使用 span 代替 vector

std::span 是 C++20 引入的一个非拥有(non-owning)的数据结构,它可以表示一个连续的内存区域,而无需分配额外的内存。如果你的数据已经存储在某个内存区域中,可以使用 std::span 来创建一个 Range,避免复制数据。

#include <span>
#include <iostream>
#include <vector>
#include <numeric>
#include <range/v3/all.hpp>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 使用 std::span 创建一个 Range
std::span<int> number_span(numbers);
auto even_squared_sum = number_span | ranges::views::filter([](int n){ return n % 2 == 0; })
| ranges::views::transform([](int n){ return n * n; })
| ranges::to<std::vector<int>>();
// int sum = std::accumulate(even_squared_sum.begin(), even_squared_sum.end(), 0);
for (auto i : even_squared_sum)
std::cout << i << std::endl;
// std::cout << "Sum of even squared numbers: " << sum << std::endl;
return 0;
}

3.3. 使用自定义分配器

C++ 允许你使用自定义分配器来控制内存的分配和释放。在嵌入式系统中,可以使用静态分配器或内存池来避免动态内存分配,从而减少内存碎片和提高性能。

#include <iostream>
#include <vector>
#include <numeric>
#include <range/v3/all.hpp>
// 自定义分配器
template <typename T>
class StaticAllocator {
public:
using value_type = T;
StaticAllocator() = default;
template <typename U>
StaticAllocator(const StaticAllocator<U>&) noexcept {}
T* allocate(std::size_t n) {
if (n > 1) throw std::bad_alloc(); // 限制每次只分配一个对象
if (!storage_) {
storage_ = new (static_memory_) T; // 使用 placement new
return reinterpret_cast<T*>(static_memory_);
}
throw std::bad_alloc();
}
void deallocate(T* p, std::size_t n) {
if (p == reinterpret_cast<T*>(static_memory_)) {
p->~T(); // 显式调用析构函数
storage_ = nullptr;
}
}
private:
alignas(T) char static_memory_[sizeof(T)]; // 静态存储区
T* storage_ = nullptr; // 指示是否已使用
};
template <typename T, typename U>
bool operator==(const StaticAllocator<T>&, const StaticAllocator<U>&) { return true; }
template <typename T, typename U>
bool operator!=(const StaticAllocator<T>&, const StaticAllocator<U>&) { return false; }
int main() {
std::vector<int, StaticAllocator<int>> numbers{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // 使用自定义分配器
auto even_squared_sum = numbers | ranges::views::filter([](int n){ return n % 2 == 0; })
| ranges::views::transform([](int n){ return n * n; })
| ranges::to<std::vector<int, StaticAllocator<int>>>();
// int sum = std::accumulate(even_squared_sum.begin(), even_squared_sum.end(), 0);
for (auto i : even_squared_sum)
std::cout << i << std::endl;
// std::cout << "Sum of even squared numbers: " << sum << std::endl;
return 0;
}

注意: 上面的 StaticAllocator 只是一个简单的示例,仅用于演示目的。在实际应用中,你需要根据你的具体需求来实现更完善的分配器。

3.4. 减少视图的组合

每个视图都会占用一定的内存,因此,要尽量减少视图的组合。如果可以将多个操作合并到一个视图中,那么就可以减少内存占用。

例如,以下代码使用了两个视图来过滤和转换数据:

auto result = numbers | ranges::views::filter([](int n){ return n % 2 == 0; })
| ranges::views::transform([](int n){ return n * n; });

可以将这两个操作合并到一个视图中:

auto result = numbers | ranges::views::transform([](int n){
if (n % 2 == 0) {
return n * n;
} else {
return 0; // 或者其他默认值
}
});

虽然这个例子中合并后的代码可读性可能稍差,但在某些情况下,为了减少内存占用,牺牲一定的可读性也是值得的。而且更重要的是,它避免了创建中间view,减少了模板具现化的数量,编译速度更快。

3.5. 避免捕获大型对象

如果你的 lambda 表达式需要捕获外部变量,要尽量避免捕获大型对象。可以考虑使用引用或指针来代替值传递,或者将大型对象存储在全局变量中。

3.6. 使用 for_each 代替 to

如果你只需要对 Range 中的元素进行迭代,而不需要将其存储到容器中,可以使用 ranges::for_each 来代替 ranges::toranges::for_each 不会创建新的容器,从而减少内存占用。

#include <iostream>
#include <vector>
#include <numeric>
#include <range/v3/all.hpp>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 使用 ranges::for_each 迭代 Range
ranges::for_each(numbers | ranges::views::filter([](int n){ return n % 2 == 0; })
| ranges::views::transform([](int n){ return n * n; }),
[](int n){ std::cout << n << std::endl; });
return 0;
}

3.7. 考虑手动循环

在某些情况下,使用手动循环可能比使用 Ranges 更有效率。虽然 Ranges 提供了更简洁的语法,但它也可能带来额外的开销。如果你的代码对性能要求非常高,可以考虑使用手动循环来代替 Ranges。

例如,以下代码使用 Ranges 计算偶数的平方和:

auto even_squared_sum = numbers | ranges::views::filter([](int n){ return n % 2 == 0; })
| ranges::views::transform([](int n){ return n * n; })
| ranges::to<std::vector<int>>();
int sum = std::accumulate(even_squared_sum.begin(), even_squared_sum.end(), 0);

可以使用手动循环来实现相同的功能:

int sum = 0;
for (int n : numbers) {
if (n % 2 == 0) {
sum += n * n;
}
}

3.8. 编译优化选项

编译器优化选项可以帮助你减少代码的内存占用和提高性能。在编译嵌入式系统代码时,可以使用 -Os 选项来告诉编译器,优先优化代码的大小。此外,还可以使用链接器优化选项来删除未使用的代码和数据。

4. 案例分析

假设我们有一个嵌入式系统,需要对传感器采集的数据进行处理。数据存储在一个 std::vector<float> 中,我们需要过滤掉噪声数据(例如,大于某个阈值或小于某个阈值的数据),然后计算剩余数据的平均值。

以下是使用 Ranges 实现的代码:

#include <iostream>
#include <vector>
#include <numeric>
#include <range/v3/all.hpp>
int main() {
std::vector<float> sensor_data = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0};
float min_threshold = 2.0;
float max_threshold = 8.0;
auto filtered_data = sensor_data | ranges::views::filter([min_threshold, max_threshold](float n){ return n >= min_threshold && n <= max_threshold; });
float sum = std::accumulate(filtered_data.begin(), filtered_data.end(), 0.0f);
int count = ranges::distance(filtered_data);
float average = sum / count;
std::cout << "Average: " << average << std::endl;
return 0;
}

这段代码使用了 ranges::views::filter 来过滤噪声数据,然后使用 std::accumulateranges::distance 来计算平均值。为了优化这段代码的内存占用,我们可以采取以下措施:

  • 使用 span 代替 vector 如果传感器数据已经存储在某个内存区域中,可以使用 std::span 来创建一个 Range,避免复制数据。
  • 避免捕获大型对象: 如果 min_thresholdmax_threshold 是大型对象,可以使用引用或指针来代替值传递。
  • 使用手动循环: 可以使用手动循环来代替 std::accumulateranges::distance,从而减少内存占用。

以下是使用手动循环优化的代码:

#include <iostream>
#include <vector>
#include <numeric>
#include <range/v3/all.hpp>
int main() {
std::vector<float> sensor_data = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0};
float min_threshold = 2.0;
float max_threshold = 8.0;
float sum = 0.0f;
int count = 0;
for (float n : sensor_data) {
if (n >= min_threshold && n <= max_threshold) {
sum += n;
count++;
}
}
float average = sum / count;
std::cout << "Average: " << average << std::endl;
return 0;
}

5. 总结

C++20 Ranges 库为嵌入式系统中的数据处理提供了新的可能性。通过合理地使用 Ranges,可以编写出更简洁、更易于维护的代码。然而,Ranges 也可能带来额外的内存开销。为了在嵌入式系统中充分利用 Ranges 的优势,需要仔细分析其内存占用情况,并采取相应的优化措施。本文提供了一系列实用的优化技巧,希望能帮助你打造高效、低内存占用的嵌入式应用。记住,在性能关键的嵌入式系统中,始终要进行实际测量和分析,以确保你的优化措施真正有效。

嵌入式老油条 C++20Ranges嵌入式系统

评论点评

打赏赞助
sponsor

感谢您的支持让我们更好的前行

分享

QRcode

https://www.webkt.com/article/9353