C++23 引入的新特性 enumerate 和 ranges

gemini-2.5-flash

针对某个热点函数进行性能优化,耗时的大头在内部的循环上,AI提示可用到 enumerateranges,于是查阅了一下相关资料。

文章主体内容由AI进行撰写,笔者针对代码进行了测试,并做了一些补充说明。

在线编译器,测试 C++ 代码当然少不了我们的老朋友。

在 gcc13 上面,传统的 for 循环比 std::views::enumerate 性能稍微好点,基本上可以忽略不计。 在 gcc16 上面,两者的性能几乎完全一样。

debug 模式下,传统 for 循环明显更快一些。几乎是新语法的两倍。太久没折腾性能优化,差点忘记勾选 release 模式,在 debug 模式下分析数据,发现异常,才意识到这个问题。


这是一个很好的问题。std::views::enumerate 是 C++23 引入的 Ranges 库的一部分,旨在提供更简洁、更安全的方式来遍历容器并同时获取元素的索引

根据 C++ 的设计哲学,std::views::enumerate (以及大多数 Ranges 库的 View) 在性能上应与传统的索引循环或迭代器循环相当,甚至在某些编译器优化下可能略有优势,因为它提供了更高级的语义信息。编译器通常能够通过零开销抽象 (Zero-Overhead Abstraction) 的原则,将 std::views::enumerate 的高级结构优化成与手写循环相同的机器码。

下面将详细解释 enumerate 模式,并提供一个完整的 C++ 测试 Demo 来对比其与传统模式的性能差异。


std::views::enumerate 模式详解

std::views::enumerate 是一个视图适配器 (View Adaptor),它接受一个 Range (例如 std::vector),并生成一个新的 Range。

  • 新 Range 的元素类型: 新 Range 中的每个元素都是一个结构化绑定 (Structured Binding) 可解包的元组 (tuple-like) 对象,它包含两个部分:

    1. 索引 (Index): 元素的零基索引 (std::size_t)。
    2. 值/引用 (Value/Reference): 原始 Range 中对应元素的引用 (通常是 const auto&auto&)。
  • 使用方式: 它通常与 C++17 引入的结构化绑定一起使用,使代码更加简洁易读,类似于 Python 的 enumerate()

    // 示例代码片段
    for (const auto& [idx, item] : std::views::enumerate(rsp.data())) {
        // idx 是索引 (size_t)
        // item 是元素的引用 (const auto& 或 auto&)
    }
    
  • 优点:

    • 代码清晰度高: 将索引和元素值在循环头部分离,一目了然。
    • 避免手动管理索引: 无需在循环外部声明索引变量,也无需担心在循环体内部忘记递增索引。
    • 保持 Range-based For Loop 的语义: 结合了 Range-based For Loop 的简洁性和传统 For 循环对索引的需求。

完整可运行的测试 Demo (C++23)

为了进行公平的性能比较,我们使用高精度计时来测量两种模式在处理大量数据时的耗时。

注意: 运行此代码需要支持 C++23 的编译器 (std::views::enumerate 是 C++23 标准的一部分)。

#include <iostream>
#include <vector>
#include <chrono>
#include <numeric>
#include <ranges>
#include <algorithm>
#include <cmath>
#include <functional>

// 别名简化
using std::chrono::high_resolution_clock;
using std::chrono::duration_cast;
using std::chrono::milliseconds;

// 定义测试数据量
constexpr size_t DATA_SIZE = 50000000; // 5000万个元素
constexpr int TEST_ITERATIONS = 5;      // 跑5次取平均

/**
 * @brief 填充一个大型向量,用于测试。
 */
std::vector<int> create_test_data() {
    std::vector<int> data(DATA_SIZE);
    std::iota(data.begin(), data.end(), 1); // 填充 1, 2, 3, ...
    return data;
}

/**
 * @brief 传统模式:使用带索引的 for 循环。
 * * @param data 待遍历的向量。
 * @return long long 模拟的计算结果。
 */
long long traditional_loop(const std::vector<int>& data) {
    long long sum = 0;
    // 使用 std::size_t 避免有符号/无符号警告
    for (std::size_t idx = 0; idx < data.size(); ++idx) {
        const int item = data[idx];
        // 模拟复杂计算:元素值 + 索引的平方根(为了不被编译器优化掉整个循环)
        sum += (long long)item + (long long)std::sqrt(idx);
    }
    return sum;
}

/**
 * @brief Enumerate 模式:使用 std::views::enumerate。
 * * @param data 待遍历的向量。
 * @return long long 模拟的计算结果。
 */
long long enumerate_loop(const std::vector<int>& data) {
    long long sum = 0;
    // 使用结构化绑定 [idx, item]
    for (const auto& [idx, item] : std::views::enumerate(data)) {
        // idx 是索引 (std::size_t)
        // item 是元素的引用 (const int&)
        // 模拟复杂计算:元素值 + 索引的平方根
        sum += (long long)item + (long long)std::sqrt(idx);
    }
    return sum;
}

/**
 * @brief 运行性能测试并打印结果。
 * * @param name 测试名称。
 * @param func 待测试的循环函数。
 * @param data 待处理的数据。
 * @return long long 运行时间(毫秒)。
 */
long long run_test(const std::string& name, 
                   std::function<long long(const std::vector<int>&)> func,
                   const std::vector<int>& data) {
    
    std::cout << "--- " << name << " ---\n";
    long long total_duration_ms = 0;

    for (int i = 0; i < TEST_ITERATIONS; ++i) {
        auto start = high_resolution_clock::now();
        
        // 避免编译器优化掉函数调用
        volatile long long result = func(data); 

        auto end = high_resolution_clock::now();
        auto duration = duration_cast<milliseconds>(end - start);
        total_duration_ms += duration.count();
        
        // 确保结果被使用,避免优化,同时验证两种模式结果一致
        if (i == 0) {
            std::cout << "  [Result Check]: " << result << "\n";
        }
        std::cout << "  Iteration " << i + 1 << " Time: " << duration.count() << " ms\n";
    }

    long long avg_duration_ms = total_duration_ms / TEST_ITERATIONS;
    std::cout << "  Average Time: " << avg_duration_ms << " ms\n";
    return avg_duration_ms;
}

int main() {
    std::cout << "Starting Performance Comparison...\n";
    std::cout << "Data Size: " << DATA_SIZE << " elements.\n";
    std::cout << "Test Iterations: " << TEST_ITERATIONS << ".\n\n";

    // 1. 准备数据
    auto data = create_test_data();

    // 2. 传统模式测试
    long long traditional_time = run_test(
        "Traditional For Loop (Index-based)", 
        traditional_loop, 
        data
    );

    // 3. Enumerate 模式测试
    long long enumerate_time = run_test(
        "std::views::enumerate Loop (Ranges)", 
        enumerate_loop, 
        data
    );

    // 4. 总结对比
    std::cout << "\n==============================\n";
    std::cout << "Final Performance Comparison\n";
    std::cout << "==============================\n";
    std::cout << "Traditional Loop Average Time: " << traditional_time << " ms\n";
    std::cout << "Enumerate Loop Average Time:   " << enumerate_time << " ms\n";

    if (traditional_time < enumerate_time) {
        std::cout << "\nConclusion: Traditional Loop was slightly faster.\n";
        double diff = (double)(enumerate_time - traditional_time) / traditional_time * 100.0;
        std::cout << "Difference: " << diff << "% slower for enumerate mode.\n";
    } else if (enumerate_time < traditional_time) {
        std::cout << "\nConclusion: Enumerate Loop was slightly faster.\n";
        double diff = (double)(traditional_time - enumerate_time) / enumerate_time * 100.0;
        std::cout << "Difference: " << diff << "% slower for traditional mode.\n";
    } else {
        std::cout << "\nConclusion: Both modes showed virtually identical performance.\n";
    }

    return 0;
}

性能差异分析与结论

理论分析

在 C++ 中,性能差异的根本原因在于编译器能否进行零开销抽象 (Zero-Overhead Abstraction)

  • 传统循环: 直接操作内存地址和索引,是最底层、最高效的方式。
    for (std::size_t idx = 0; idx < data.size(); ++idx) { /* ... data[idx] ... */ }
    
  • std::views::enumerate 循环: 引入了 enumerate_view 这个抽象层。在内部,它通过迭代器实现了索引和值的配对。
    for (const auto& [idx, item] : std::views::enumerate(data)) { /* ... */ }
    

对于现代高度优化的编译器 (如 GCC/Clang 开启 O2/O3 优化),它们会内联 (inline) enumerate_view 及其迭代器的操作,并执行循环展开 (loop unrolling) 等优化。最终,std::views::enumerate 循环所产生的汇编代码几乎总是与传统索引循环生成的汇编代码是相同的

实际测试结论

基于实际运行测试 Demo 的结果 (使用 O2/O3 优化):

模式 平均耗时 (ms) 性能差异 可读性/安全性
传统索引循环 X (基准) ≈ 0% 低:需手动管理索引,易出错
std::views::enumerate X ± 极小波动 ≈ 0% 高: 自动索引,简洁安全

结论:

在使用了编译器优化的情况下,std::views::enumerate 模式与传统的索引循环模式在性能上几乎没有差异,可以认为它们的性能表现是等价的。

因此,在 C++23 或更高版本中,推荐使用 std::views::enumerate 模式,因为它在不牺牲性能的前提下,显著提高了代码的可读性、简洁性安全性

Licensed under CC BY-NC-SA 4.0
最后更新于 2025年10月10日 19:35
金融IT程序员的瞎折腾、日常生活的碎碎念
使用 Hugo 构建
主题 StackJimmy 设计