C++ std::ranges性能陷阱解析:为何函数式编程可能不如传统循环

本文深入分析C++20中std::ranges的性能表现,通过实际代码对比和基准测试,揭示函数式编程风格可能带来的性能开销,为开发者提供实用的性能优化建议和编程选择指导。

std::ranges可能无法达到你期望的性能表现

优秀的工程师追求代码的"简洁性",即能够快速阅读和理解。但他们同时也追求高性能的代码。

在过去的20年中,我们一直为程序员提供用更函数式的方法替代传统for循环的可能性。举例来说,假设你想从容器中提取所有偶数并创建新容器。在传统C++中,你会使用循环:

1
2
3
4
5
6
std::vector<int> even_numbers;
for (int n : numbers) {
  if (n % 2 == 0) {
    even_numbers.push_back(n);
  }
}

在较新的C++版本中,我们有std::ranges,可以让我们不用for循环重写代码:

1
2
3
auto even_numbers = numbers 
    | std::views::filter([](int n) { return n % 2 == 0; })
    | std::ranges::to<std::vector>();

其背后的魔力在于filter是惰性的,它不会遍历整个输入并生成临时结果再传递出去。这允许你将一系列转换操作串联起来。你还可以对结果求和,并且在不久的将来,我们甚至将获得并行执行能力。可能有人已经写了一本关于std::ranges的书。

在这方面,C++并不独特。即使是Java也多年来一直提供类似的构造。

像std::ranges这样的抽象背后的原则是,我们可以在不妥协的情况下获得完整性能。遗憾的是,这样的承诺应该谨慎对待。

本周我在一家本地C++公司做了演讲。有人问我关于C++ ranges的性能开销问题。当他们将编译器切换到C++20时,一些工程师尝试使用std::ranges并引发了性能下降。

在我们的快速C++ JSON解析器(simdjson)中,到目前为止,我们将std::ranges支持限制在库的特定部分……不是因为我们无法使其工作,而是因为它导致了性能下降。明确地说,我相信我们能够使其工作,但这并不简单。重要的是,在发布代码之前我们进行了基准测试。

为了说明潜在的性能陷阱,我选择了在网上找到的第一个例子:如何修剪字符串开头和结尾的空格。

1
2
3
4
s | std::views::drop_while(is_space) 
                    | std::views::reverse 
                    | std::views::drop_while(is_space) 
                    | std::views::reverse;

这个C++代码片段处理字符串s,修剪两端的空白字符而不修改原始字符串,返回修剪结果的惰性视图。它首先应用std::views::drop_while(is_space)来移除任何前导空白。然后,std::views::reverse反转剩余内容,将原始尾部空白转换为前导空白。接着,另一个std::views::drop_while(is_space)丢弃这个新的前导空白(实际上是原始的尾部部分),最后的std::views::reverse恢复原始顺序。

明确地说,这只是使用std::ranges完成任务的一种方式,我选择它只是因为它是我在网上首先找到的。代替这种函数式代码,我们可以使用两个循环的更混乱的方法:

1
2
3
4
5
6
7
while (!input.empty() &&
         is_space(input.front())) {
    input.remove_prefix(1);
}
while (!input.empty() && is_space(input.back())) {
    input.remove_suffix(1);
}

那么性能如何变化呢?我编写了一个基准测试来比较这些函数。我使用不包含空格的随机字符串。我记录每个处理字符串所需的指令数。我使用了两个不同的C++编译器和两个不同的处理器。

函数 LLVM 17/Apple M4 GCC 15/Intel IceLake
std::ranges 24 70
传统方法 18 16

在我的测试中,我发现传统函数更快,因为它生成的指令更少。在GCC的情况下,差异很大。如果你感兴趣,可以查看GCC如何编译这些函数。

这是否意味着std::ranges是个坏主意或者设计不佳?不。这意味着没有神奇的精灵能够开箱即用地给你最佳性能。使用std::ranges,但要进行基准测试,基准测试,再基准测试。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计