从Vec遍历的性能陷阱,聊聊Rust的零拷贝迭代器模式

Rust 03: Vec列表、二维列表、子列表、列表切片 rus中Vec列表/二维列表的创建、遍历、排序,列表切片。 列表可以当成Stack来使用。 阅读详情

在这里插入图片描述

最近在优化一个数据处理服务时,我发现了一个有意思的现象:同样是遍历Vec,不同的写法性能差距竟然能达到30%。这个发现让我深入研究了Rust的零拷贝迭代器模式,也让我重新理解了Rust"零成本抽象"的真正含义。

从一个性能问题说起

那是个普通的周三下午,我正在处理一个需要解析大量日志文件的需求。代码很简单,就是遍历一个包含几万个字符串的Vec,提取其中的时间戳。第一版代码我是这么写的:

fn process_logs_v1(logs: Vec<String>) -> Vec<i64> {
    let mut timestamps = Vec::new();
    for i in 0..logs.len() {
        let log = &logs[i];
        if let Some(ts) = extract_timestamp(log) {
            timestamps.push(ts);
        }
    }
    timestamps
}

看起来没什么问题对吧?但当我用cargo bench跑性能测试时,发现处理10万条日志竟然要200多毫秒。我心想不对啊,这只是简单的字符串解析,怎么会这么慢?

问题的根源:隐藏的边界检查

我用cargo asm查看了生成的汇编代码,发现了端倪。每次logs[i]访问时,Rust都插入了边界检查的代码。虽然单次检查很快,但积少成多,10万次循环就意味着10万次不必要的检查。

这时候我想到了迭代器。把代码改成这样:

fn process_logs_v2(logs: Vec<String>) -> Vec<i64> {
    logs.iter()
        .filter_map(|log| extract_timestamp(log))
        .collect()
}

重新跑benchmark,时间降到了140毫秒左右。提升了30%!但这还不是重点,重点是我开始思考:为什么迭代器会更快?

零拷贝的本质:所有权的智慧

传统语言的迭代器往往会复制元素。比如在C++中,如果你不小心写成for(auto item : vec),每个item都是复制出来的。但Rust的迭代器不一样。

当你写logs.iter()时,它返回的是一个Iter<String>类型。这个迭代器并不拥有Vec中的数据,它只是持有Vec的不可变引用,然后依次返回每个元素的引用。整个过程,数据本身一动不动,没有任何拷贝发生。

我写了个简单的例子来验证:

fn test_zero_copy() {
    let data = vec![String::from("hello"), String::from("world")];
    
    // 迭代器只是引用,不会移动数据
    for item in data.iter() {
        println!("地址: {:p}, 内容: {}", item, item);
    }
    
    // 迭代后,data依然可用
    println!("data还在: {:?}", data);
}

运行这段代码,你会发现打印出的地址指向的正是Vec内部的数据。迭代器只是个"导游",带你看遍所有数据,但从不搬动它们。

三种迭代器:各有千秋

在实际开发中,我发现Rust提供了三种迭代器,每种都有特定的使用场景:

iter() - 不可变借用迭代器

这是最常用的。当你只需要读取数据时用它。比如我统计日志中错误的数量:

fn count_errors(logs: &Vec<String>) -> usize {
    logs.iter()
        .filter(|log| log.contains("ERROR"))
        .count()
}

iter_mut() - 可变借用迭代器

需要修改元素时使用。我曾经用它批量处理用户输入,去除首尾空格:

fn trim_all(texts: &mut Vec<String>) {
    texts.iter_mut()
         .for_each(|text| *text = text.trim().to_string());
}

into_iter() - 所有权转移迭代器

这个最特殊。它会消耗掉原Vec,取得每个元素的所有权。我在处理一次性数据转换时用它:

fn convert_to_uppercase(logs: Vec<String>) -> Vec<String> {
    logs.into_iter()
        .map(|s| s.to_uppercase())
        .collect()
}

这里有个细节值得注意。into_iter()的map闭包接收的是String,而非&String。这意味着我们可以直接消费原字符串,避免了额外的克隆。

温馨提示: 使用into_iter()后,原Vec就不能再用了。这是Rust所有权系统在保护你,避免use-after-free的问题。

迭代器适配器:组合的魔法

真正让我感受到零拷贝威力的,是迭代器适配器的组合使用。回到最开始的日志处理场景,我后来又加了新需求:只处理最近一小时的日志,且要去重。

用传统方式写可能要好几个循环,但用迭代器:

use std::collections::HashSet;

fn process_recent_logs(logs: Vec<String>) -> Vec<i64> {
    let one_hour_ago = current_timestamp() - 3600;
    let mut seen = HashSet::new();
    
    logs.iter()
        .filter_map(|log| extract_timestamp(log))
        .filter(|&ts| ts > one_hour_ago)
        .filter(|ts| seen.insert(*ts))  // 去重
        .collect()
}

这段代码的精妙之处在于:整个处理链条中,日志字符串只被引用,从未被复制。filter_map、filter这些适配器都是惰性的,它们只是在组装处理流程,并不立即执行。只有当最后collect()时,才会一次性遍历处理。

我做过测试,这种链式处理比分步骤处理要快15%左右。原因是编译器能更好地优化这种函数式风格的代码,甚至有时能做到内联,完全消除函数调用开销。

自定义迭代器:深入理解原理

为了彻底理解零拷贝迭代器,我尝试自己实现了一个。假设我有个只存储偶数的数据结构:

struct EvenNumbers {
    data: Vec<i32>,
}

impl EvenNumbers {
    fn new() -> Self {
        EvenNumbers { data: Vec::new() }
    }
    
    fn add(&mut self, num: i32) {
        if num % 2 == 0 {
            self.data.push(num);
        }
    }
}

现在要为它实现迭代器:

struct EvenIter<'a> {
    inner: std::slice::Iter<'a, i32>,
}

impl<'a> Iterator for EvenIter<'a> {
    type Item = &'a i32;
    
    fn next(&mut self) -> Option<Self::Item> {
        self.inner.next()
    }
}

impl EvenNumbers {
    fn iter(&self) -> EvenIter {
        EvenIter {
            inner: self.data.iter(),
        }
    }
}

注意看生命周期参数'a。这是Rust确保零拷贝安全性的关键。EvenIter<'a>的生命周期不能超过EvenNumbers,编译器会在编译期就检查这一点。

使用起来就很自然了:

fn main() {
    let mut evens = EvenNumbers::new();
    evens.add(2);
    evens.add(4);
    evens.add(6);
    
    for num in evens.iter() {
        println!("偶数: {}", num);
    }
}

整个过程,数据一直待在EvenNumbers的Vec里,迭代器只是提供了一个访问窗口。

温馨提示: 自定义迭代器时,生命周期参数很容易写错。记住一个原则:迭代器返回的引用,生命周期要与被迭代的数据结构绑定。

性能对比:实测数据说话

我专门写了个benchmark来量化零拷贝迭代器的性能优势:

use std::time::Instant;

fn benchmark() {
    let data: Vec<String> = (0..100000)
        .map(|i| format!("log_entry_{}", i))
        .collect();
    
    // 方式1: 索引访问
    let start = Instant::now();
    let mut count1 = 0;
    for i in 0..data.len() {
        if data[i].len() > 10 {
            count1 += 1;
        }
    }
    println!("索引方式: {:?}", start.elapsed());
    
    // 方式2: 迭代器
    let start = Instant::now();
    let count2 = data.iter()
        .filter(|s| s.len() > 10)
        .count();
    println!("迭代器方式: {:?}", start.elapsed());
}

在我的机器上,索引方式约2.1ms,迭代器方式约1.4ms。差距明显。

更重要的是,迭代器方式的代码更简洁、更不容易出错。没有索引越界的风险,没有off-by-one错误的可能。

实战经验:避开这些坑

经过几个月的实践,我总结了几条使用零拷贝迭代器的经验:

别过度使用collect()

新手容易写出这样的代码:

// 不好的写法
let result: Vec<_> = data.iter()
    .filter(|x| x.is_valid())
    .collect();
let final_result: Vec<_> = result.iter()
    .map(|x| x.process())
    .collect();

这会创建中间Vec。应该这样:

// 更好的写法
let final_result: Vec<_> = data.iter()
    .filter(|x| x.is_valid())
    .map(|x| x.process())
    .collect();

理解迭代器的惰性求值

迭代器在没有消费者时不会执行:

let _ = data.iter().map(|x| {
    println!("处理: {}", x);  // 这行不会执行!
    x
});

必须有collect()、for_each()或count()这样的消费者。

小心闭包的捕获

在迭代器链中使用闭包时,注意变量捕获的方式:

let threshold = 100;
data.iter()
    .filter(|x| x.value > threshold)  // threshold被不可变借用
    .collect()

如果需要修改外部变量,考虑使用fold()而非for_each()。

写在最后

零拷贝迭代器是Rust"零成本抽象"理念的完美体现。它让你用高层次的函数式风格写代码,编译器却能生成和手写循环一样快甚至更快的机器码。

从最初被性能问题困扰,到现在能熟练运用迭代器优化代码,我最大的感悟是:Rust不仅仅是让你写出安全的代码,更是在引导你写出优雅且高效的代码。每一个语言特性背后,都有深刻的设计考量。

当你下次遍历集合时,不妨停下来想想:我真的需要拷贝这些数据吗?也许一个简单的.iter()就够了。代码会感谢你的。

<数据集>咖啡叶片病害识别<目标检测> YOLO与VOC格式的咖啡叶片病害识别数据集,适用于YOLO系列、Faster Rcnn、SSD等模型训练,类别:Cercospora、Miner、Phoma、Rust,图片数量1744。文件中包含图片、txt标签、指定类别信息的yaml文件、xml标签,已将图片和txt标签划分为训练集、验证集和测试集,可直接用于YOLOv5、YOLOv6、YOLOv7、YOLOv8、YOLOv9、YOLOv10、YOLOv11、YOLOv12、YOLOv13、YOLOv26等YOLO系列算法的训练。 阅读详情

相关推荐

UVM入门与进阶学习笔记17——寄存器模型(3)

目录寄存器模型的常规方法mirror、desired和actual valueprediction的分类自动预测显式预测uvm_reg的访问方法mem与reg的联系和差别内建(built-in)sequences寄存器模型内建序列存储模型内建序列寄存器健康检查寄存器应用场景寄存器检查功能覆盖率概述覆盖率自动收集模式覆盖率外部事件触发收集 寄存器模型的常规方法 mirror、desired和actual value 在应用寄存器模型时,除了利用它的寄存器信息,也会利用它跟踪寄存器的值。 寄存器模型中的每

weixin_41979380的博客 7536

Rust学习总结之vector

Rust学习总结之Vector,一篇就够了

ftzchina的博客 1562

链路监控pinpoint3.0.3安装部署(2)

本文详细介绍了Pinpoint 3.0.3分布式链路监控系统的安装部署过程。主要内容包括:1)数据库安装(HBase、MySQL、Redis、Pinot)的详细配置步骤;2)中间件Zookeeper和Kafka的安装;3)Pinpoint三大组件的部署:Collector、Web和Batch的配置文件修改和启动脚本。文章特别强调了使用-starter-exec.jar包的重要性,并提供了各组件所需的详细参数配置,包括数据库连接、Zookeeper地址、Kafka配置等关键信息。

zouzou_888的博客 1374

Rust从入门到实战系列七十五:遍历 vector 中的元素

例如,假如我们想要从电子表格的一行中获取值,而这一行的有些列包含数字,有些包含浮点值,还有些是字符串。我们可以定义一个枚举,其成员会存放这些不同类型的值,同时所有这些枚举成员都会被当作相同类型,那个枚举的类型。绝对会有需要储存一系列不同类型的值的用例。幸运的是,枚举的成员都被定义为相同的枚举类型,所以当需要在 vector 中储存不同类型值时,我们可以定义并使用一个枚举!如果想要依次访问 vector 中的每一个元素,我们可以遍历其所有的元素而无需通过索引一次一个的访问。

wanmeijuhao的博客 879

国税局发票查验中英文验证码识别,识别率95.2%

国税验证码识别方法介绍

onedayismyway的博客 3134

ADS软件操作(10)——DAC控件使用

该控件可以用于为元件定义随频率变换的参数,如放大器的最佳负载阻抗或者源阻抗,需要双频匹配时的优化等,下图是ADS中该控件的模型。

Zhuanzhuyan的博客 4395

rust使用vec在遍历时删除元素

rust使用vec在遍历时删除元素 需求: 工作中有一个地方需要用到在遍历时把不符合条件的元素删除掉, 比如一个vec中是原始数据是 vec![1,2,3,3,4,5] ,然后我要在遍历中把等于c的元素删除掉,目的是得到vec![1,2,4,5] 第一次错误尝试 由于最开始只知道移除元素用remove方法,所以最开始是这样写的 let mut items:Vec<&str> = vec!["a", "b", "c", "c", "d", "e"]; println!(

IP大侠的博客 4259

数据结构——滑动窗口

入:下标为 i 的元素进入窗口,更新相关统计量。如果 i < k−1 则重复第一步更新:更新答案。一般是更新最大值 / 最小值出:下标为 i−k+1 的元素离开窗口,更新相关统计量✔1. 定长子串中元音的最大数目✔2. 找到一个数字的 K 美丽值✔4. 子数组的最大平均数 |方法一:前缀和方法二:滑动窗口7. 得到 K 个黑块的最少涂色次数8. 爱生气的书店老板9. 几乎唯一子数组的最大和10. 长度为 K 子数组中的最大和11. 可获得的最大点数12. 最少交换次数来组合所有的1 ||

qq_68915288的博客 762

rust学习笔记

原文链接:https://blog.youkuaiyun.com/qq_41930516/article/details/128841400。版权声明:本文为优快云博主「Java养家」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。1、动态数组vec遍历。

jiangchao04的博客 497

王炸联动!OpenClaw 对接微信 / 企业微信保姆级教程,AI 办公效率翻倍

《OpenClaw对接微信/企业微信全指南》摘要:本文提供开源AI智能体OpenClaw与微信生态的完整对接教程。核心内容包括企业微信配置四步法(获取凭证、修改配置、重启服务、验证对接)和个人微信通过企业微信插件的一键打通方案。对接后支持微信/企微发指令AI秒执行、群聊机器人、文件同步等高效办公场景,并附常见问题排查代码。教程强调配置简单(全程可复制代码)、适用广泛(个人/团队场景),实现沟通与AI协作的无缝衔接。文末包含端口放行等实用命令,助力用户快速实现移动端AI办公。(149字)

2501_93651177的博客 584

078、镜头阴影校正:LSC算法原理与标定优化

文章摘要 镜头阴影校正(LSC)是手机影像系统中的关键技术,用于解决镜头渐晕效应导致的画面边缘暗角问题。本文详细分析了LSC算法的核心原理,包括非均匀网格划分、双线性插值等关键技术点,并揭示了标定流程中的常见陷阱(光源均匀性、灰卡反射特性、温度漂移)。文章还深入探讨了增益计算的工程实现细节,以及实际调优中的关键考量因素(色差校正、动态场景适应等)。作者结合15年经验,给出了包括验证方法、网格参数设计、产线标定等实用性建议,强调LSC算法应与光学设计协同优化,才能达到最佳效果。

sundong_81的博客 42

[Rust学习:四] Vec和栈

rust的vector和栈

liuliangcan的博客 1753

国科大学习资料--人工智能原理与算法-第六次作业解析(学长整理)

国科大学习资料–人工智能原理与算法-第六次作业解析(张文生老师主讲)(6.6、6.7、6.11) 6.6 说明如何通过使用辅助变量把诸如A+B=C这样的三元约束转变成三个二元约束。假设值域是有限的。(提示:考虑引进新变量表示变量对的值,引进约束如“X是Y变量对中的第一个元素”。)然后说明如何把多于三个变量的约束类似地转换为二元约束。说明如何通过改变变量的值域来消除一元约束。这就完整演示了任何CSP都可以转变为只含二元约束的CSP。 答:首先,我们引入一个新变量AB,假设A和B的范围均为N,AB的范围为N×N

无名何许人的博客 2870

02-Rust常用集合(遍历数组并获取下标最佳方式)

注重细节: Rust常用集合(遍历数组并获取下标最佳方式)

qq_41930516的博客 2053

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

Rust开发——Vec与Struct的使用示例

Vec是Rust的可容器,Struct是结构体,也可以当类使用,这两个是Rust开发中比较常用的两个知识点,下面例子是结合ec与Struct,对ec里的Struct结构元素进行查找、排序、筛选等操作。

知来者逆的博客 1216

迈瑞BS系列全自动生化分析仪LIS协议接口手册(内含HL7和ASTM)

迈瑞BS系列全自动生化分析仪LIS协议接口手册,内含HL7和ASTM)

rust Vec 常用操作

Vec的一些常用操作: 声明,增删改查,判空,排序,比较,并集,搜索 fn main() { let mut vec2: Vec<i32> = Vec::new(); println!(" 声明一个空Vec {:?} , 长度 {} ", vec2, vec2.len()); let mut vec3 = [1].repeat(5); println!(" 声明一个重复元素的Vec: {:?}", vec3); let mut v...

阿昊的博客 1万+

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

Rust开发——Vec向量

向量是可调整大小的数组,可用于动态存储元素序列的数据结构。要求存储的元素必须具有相同的类型。如果需要存储不用类型,可以选择使用带有多个变量的枚举作为在向量中存储不同类型元素的方法...

知来者逆的博客 4218

rust 迭代器 (iterator) 详解

展示对rust iterator基本用法 这篇文章的目的是为一些常见的iterator提供参考资料。并不能替代Iterator API或者书中的Rust iterator核心概念 ,事实上这本书的内容来自以上两种内容。 为了更好理解本篇文章内容,推荐读者至少粗略了解Rust 如何编译运行例子git clone https://github.com/rustomax/rust-iterato

归去来兮的博客 2万+

Rust: Vec用法及其它

Rust中的狭义的vector,相当于一些其它语言可变长度的List,ArrayList,Array(Julia语言),是核心的数据结构。

代码过客 2万+

Rust:Rust中的集合类型之vectors

Vec,也称为vector。它允许我们在一个单独的数据结构中存储一系列相同类型的值,它们在内存中彼此相邻的排列所有的值。 文章目录vector VS 数组vector初始化引用元素:&amp;v[index] 对比 v.get(index)循环修改元素存储不同类型的元素:vector结合enum vector VS 数组 不同点 vector是存储在堆上,数组存储在栈上 vector的长度是...

OceanStar的博客 6504

GitHub 热榜项目:日榜(2026-11-07)

给编码智能体配技能的项目最热:cloudflare/security-audit-skill 把审计拆成六阶段并输出可验证 JSON,新增 2,548 星;mattpocock/skills 把工程技能放进 .agents 目录,新增 2,070 星。Web 应用框架也有四个项目集中上榜,以 react/react 为代表。

u130130的博客 386

GitHub 热榜项目:日榜(2026-11-06)

给编码智能体补工程与产品技能是本期最明显的一类,mattpocock/skills 把需求对齐、TDD、缺陷诊断做成小技能。NVIDIA/auto-ontology 从企业元数据生成本体、构建知识图谱,为 LLM 提供带来源追溯的自然语言查询和 SQL 生成,新增 2,679 星;vercel-labs/agent-desktop 通过桌面无障碍树把界面元素转成 JSON 和可操作 ref,让智能体不必截图就能点击输入,新增 2,173 星。

u130130的博客 456

【Rust 手搓 Win11 灵动岛】① 从零搭骨架:无边框置顶窗口 + 工作区精确停靠

项目“岛仔”(daozai)是基于 Rust 与 eframe/egui 构建的 Win11 灵动岛桌面应用,采用窗口恒定展开、胶囊自绘方案实现无抖动丝滑动画。本篇聚焦 M1 里程碑:通过 Win32 获取工作区避让任务栏,解决创建期 DPI 缩放误传问题——虽首帧补偿逻辑因误解 ViewportCommand 单位而引入双重缩放错误,但实测假阳性掩盖了缺陷;后续将修复为直接下发 logical 尺寸。代码重构提升可测性,三组单元测试覆盖多 DPI 场景。最终验证结果经更正后确认需在下篇修正。

ygn12的博客 346

OpenSearch 的索引快照别只留本地:用 repository-s3 存进 RustFS

OpenSearch 跑起来,索引分片默认写在节点自己的本地盘上。单机测试很顺,可一旦索引量上来、节点不止一个,本地盘的故障就成了绕不开的坎:一块盘坏了,上面的分片副本丢了,重建全靠其他副本兜着,集群压力瞬间翻倍。更隐蔽的是快照,很多人把快照也留在本地盘的默认仓库里,等于把备份和原始数据放在了同一块会坏的盘上。OpenSearch 自带 repository-s3 插件,能把索引快照直接写进任意…

分布式存储与RustFS的博客 562

Rolldown 把插件改动回传提速 26 倍

Rolldown 下一个版本会把插件改动的回传做细,只把真的改了的字段送回 Rust,大型项目里这一步从 1015 毫秒降到 39 毫秒。什么时候能用上呢,改动已经在 11 月 6 日合并,会跟着下一个版本走,Vite 8 用的就是 Rolldown。更麻烦的是缓存,Rust 替换 chunk 数据之后,之前那份代码缓存就废了,下一个读。这次没有动钩子对象本身,只是把「回传」这件事做细了,先看哪些字段变了,再决定传什么。所以改法很直接,只送变了的字段,没变的 chunk 一个字节都不发。

前端之虎陈随易 193

我写了一个 Windows 开发环境管理平台:Nexus(Tauri 2 + Rust + React)

文件监听事件很密集。如果节流窗口内的变更直接丢弃,最后一次改动就永远加载不到——所以窗口结束时必须补一轮。只对「失败」重试,不对「启动中」重试。启动慢不代表启动失败,盲目重试只会把健康的服务越搞越糟。Nexus 原本是为了解决自己日常开发里「窗口太多、状态不清、日志找不到」的问题,后来慢慢补成了现在这个样子:项目与服务管理、日志、服务健康、文件与代码查看、文件监听、开机启动、Node 版本管理,以及一个内嵌的 AI 助手。如果你也在 Windows 上同时维护多个服务,欢迎试用。

hi星尘的博客 565
上一篇: Rust智能指针的奇妙之旅:从踩坑到顿悟
下一篇: 从迭代器的“变身术“说起:深入理解 Rust 的 IntoIterator trait
剑指~巅峰
博客等级 码龄3年 323粉丝 18原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值