改进rust代码的35种具体方法-类型(二十)-避免过度优化的诱惑

devtools/2024/10/18 20:21:08/

上一篇文章-改进rust代码的35种具体方法-类型(十九)-避免使用反射


“仅仅因为Rust允许您安全地编写超酷的非分配零复制算法,并不意味着您编写的每个算法都应该是超级酷的、零复制和非分配的。”-trentj

     这本书中的大多数项目都旨在帮助现有程序员熟悉Rust及其成语。然而,这个项目是关于一个问题,当程序员偏离另一个方向太远,痴迷于利用Rust的效率潜力时,可能会出现这个问题——以牺牲可用性和可维护性为代价。 

数据结构和分配

与其他语言中的指针一样,Rust的引用允许您在不复制的情况下重用数据。与其他语言不同,Rust关于引用生命周期和借阅的规则允许您安全地重用数据。然而,遵守使这成为可能的借款检查规则可能会导致代码更难使用。

这与数据结构特别相关,您可以选择分配存储在数据结构中的新副本或包含对现有副本的引用。

例如,考虑一些解析字节数据流的代码,提取编码为类型长度值(TLV)结构的数据,其中数据以以下格式传输:

  • 描述值类型的一个字节(存储在这里的type_code字段中)1
  • 一个字节描述以字节为单位的值长度(此处用于创建指定长度的切片)
  • 后跟值的指定字节数(存储在value字段中):
rust">/// A type-length-value (TLV) from a data stream.
#[derive(Clone, Debug)]
pub struct Tlv<'a> {pub type_code: u8,pub value: &'a [u8],
}pub type Error = &'static str; // Some local error type./// Extract the next TLV from the `input`, also returning the remaining
/// unprocessed data.
pub fn get_next_tlv(input: &[u8]) -> Result<(Tlv, &[u8]), Error> {if input.len() < 2 {return Err("too short for a TLV");}// The TL parts of the TLV are one byte each.let type_code = input[0];let len = input[1] as usize;if 2 + len > input.len() {return Err("TLV longer than remaining data");}let tlv = Tlv {type_code,// Reference the relevant chunk of input datavalue: &input[2..2 + len],};Ok((tlv, &input[2 + len..]))
}

这种Tlv数据结构是高效的,因为它持有对输入数据相关块的引用,而不会复制任何数据,并且Rust的内存安全确保了引用始终有效。这对于某些场景来说是完美的,但如果某些东西需要挂在数据结构的实例上,事情会变得更加尴尬。

例如,考虑一个以TLV形式接收消息的网络服务器。接收的数据可以解析为Tlv实例,但这些实例的生命周期将与传入消息的生命周期相匹配——该消息可能是堆上的瞬态Vec<u8>,也可能是重复用于多个消息的缓冲区。

如果服务器代码想要存储传入消息,以便稍后查阅,这会导致问题:

rust">pub struct NetworkServer<'a> {// .../// Most recent max-size message.max_size: Option<Tlv<'a>>,
}/// Message type code for a set-maximum-size message.
const SET_MAX_SIZE: u8 = 0x01;impl<'a> NetworkServer<'a> {pub fn process(&mut self, mut data: &'a [u8]) -> Result<(), Error> {while !data.is_empty() {let (tlv, rest) = get_next_tlv(data)?;match tlv.type_code {SET_MAX_SIZE => {// Save off the most recent `SET_MAX_SIZE` message.self.max_size = Some(tlv);}// (Deal with other message types)// ..._ => return Err("unknown message type"),}data = rest; // Process remaining data on next iteration.}Ok(())}
}

此代码按原样编译,但实际上无法使用:NetworkServer的生命周期必须小于输入其process()方法的任何数据的生命周期。这意味着一个简单的处理循环:

rust">let mut server = NetworkServer::default();
while !server.done() {// Read data into a fresh vector.let data: Vec<u8> = read_data_from_socket();if let Err(e) = server.process(&data) {log::error!("Failed to process data: {:?}", e);}
}

无法编译,因为临时数据的生命周期被附加到寿命较长的服务器上:

rust">error[E0597]: `data` does not live long enough--> src/main.rs:375:40|
372 |     while !server.done() {|            ------------- borrow later used here
373 |         // Read data into a fresh vector.
374 |         let data: Vec<u8> = read_data_from_socket();|             ---- binding `data` declared here
375 |         if let Err(e) = server.process(&data) {|                                        ^^^^^ borrowed value does not live|                                              long enough
...
378 |     }|     - `data` dropped here while still borrowed

切换代码以便重用寿命更长的缓冲区也无济于事:

rust">let mut perma_buffer = [0u8; 256];
let mut server = NetworkServer::default(); // lifetime within `perma_buffer`while !server.done() {// Reuse the same buffer for the next load of data.read_data_into_buffer(&mut perma_buffer);if let Err(e) = server.process(&perma_buffer) {log::error!("Failed to process data: {:?}", e);}
}

这一次,编译器抱怨代码试图挂在引用的同时,同时向同一缓冲区分发可变引用:

rust">error[E0502]: cannot borrow `perma_buffer` as mutable because it is alsoborrowed as immutable--> src/main.rs:353:31|
353 |         read_data_into_buffer(&mut perma_buffer);|                               ^^^^^^^^^^^^^^^^^ mutable borrow occurs here
354 |         if let Err(e) = server.process(&perma_buffer) {|                         -----------------------------|                         |              ||                         |              immutable borrow occurs here|                         immutable borrow later used here

核心问题是Tlv结构引用瞬态数据——这对于瞬态处理很好,但从根本上与以后的存储状态不兼容。但是,如果Tlv数据结构被转换为拥有其内容:

rust">#[derive(Clone, Debug)]
pub struct Tlv {pub type_code: u8,pub value: Vec<u8>, // owned heap data
}

那么服务器代码的工作要容易得多。拥有数据的Tlv结构没有生命周期参数,因此服务器数据结构也不需要一个参数,并且处理循环的两个变体都可以正常工作。

谁害怕大复制?

程序员可能过于痴迷于减少副本的一个原因是,Rust通常会明确复制和分配。对.to_vec().clone()等方法或Box::new()等函数的可见调用,清楚地表明正在发生复制和分配。这与C++形成鲜明对比,在C++中很容易无意中编写代码,在封面下轻松执行分配,特别是在复制构造函数或赋值运算符中。

使分配或复制操作可见而不是隐藏不是优化它的好理由,特别是如果这种情况以牺牲可用性为代价。在许多情况下,如果性能确实令人担忧,并且基准测试表明减少副本将产生重大影响,首先关注可用性,并进行微调以获得最佳效率,这更有意义。

此外,只有当代码需要扩展以供广泛使用时,代码的效率通常才重要。如果事实证明代码中的权衡是错误的,并且当数百万用户开始使用它时,它无法很好地应对——好吧,这是一个好问题。

然而,有几个具体要点需要记住。当指出副本通常可见时,第一个通常隐藏在錍��豆豆字后面。最大的例外是Copy类型,编译器会随意无声地复制,从移动语义转向复制语义。因此,在第十篇中的建议值得在这里重复:除非按位副本有效且快速,否则不要实现Copy。但反过来也是这样:如果按位的副本有效且快速,请考虑实现Copy。例如,如果派生Copy,不携带额外数据的enum类型通常更容易使用。

可能相关的第二点是使用no_std的潜在权衡。通常只需稍加修改即可编写与no_std兼容的代码,而完全避免分配的代码则使其更加简单。然而,针对支持堆分配的anono_std环境(通过alloc库),可能会提供可用性和no_std支持的最佳平衡。

参考和智能指针

“所以最近,我有意识地尝试了不担心假设的完美代码的实验。相反,我在需要时调用.clone(),并使用Arc更顺利地将本地对象放入线程和未来。

感觉很光荣。”-乔什·特里普利特

设计一个数据结构,使其拥有其内容当然可以更好地实现人体工程学,但如果多个数据结构需要使用相同的信息,仍然存在潜在问题。如果数据是不可变的,那么每个拥有自己副本的地方都可以正常工作,但如果信息可能会发生变化(这种情况很常见),那么多个副本意味着需要更新的多个地方,彼此同步。

使用Rust的智能指针类型有助于解决这个问题,允许设计从单所有者模型转移到共享所有者模型。Rc(用于单线程代码)和Arc(用于多线程代码)智能指针提供支持这种共享所有权模型的参考计数。继续假设需要可变性,它们通常与允许内部可变性的内部类型配对,独立于Rust的借入检查规则:

借用检查器的GuestRegister示例更详细地介绍了这一过渡,但这里的重点是,您不必将Rust的智能指针视为最后手段。如果您的设计使用智能指针而不是相互连接的参考寿命的复杂网络,这并不是失败的承认——智能指针可以导致更简单、更易于维护、更可用的设计


1该字段不能被命名为type,因为这是Rust中的保留关键字。可以使用原始标识符前缀r#(给一个字段r#type: u8)来绕过此限制,但通常只需重命名字段就更容易了。


http://www.ppmy.cn/devtools/45249.html

相关文章

网络协议学习笔记

HTTP协议 简单介绍 HTTP属于应用层 HTTP可以简单的理解成类似json一样的文本封装&#xff0c;但是这是超文本&#xff0c;所以可以封装的不止有文本&#xff0c;还有音视频、图片等 请求方法 HTTP报文格式 三大部分 起始行&#xff1a;描述请求或响应的基本信息头部字段…

Sqoop与Shell脚本数据迁移实战

文章目录 前言一、sqoop实战示例1. 获取所有数据库2. 获取指定数据库的所有表3. 查询数据4. 把指定数据库的所有表导入指定hive数据库5. 把指定表导入hive数据库的指定表6. 查询数据导入到指定表 二、shell脚本实战示例1. shell脚本2. 解释 总结 前言 在数据驱动的时代&#x…

前端经典手写面试题---节流防抖

防抖 定义: n 秒后在执行该事件&#xff0c;若在 n 秒内被重复触发&#xff0c;则重新计时。 场景: 搜索框搜索输入。只需用户最后一次输入完&#xff0c;再发送请求手机号、邮箱验证输入检测窗口大小resize。只需窗口调整完成后&#xff0c;计算窗口大小。防止重复渲染。 实…

国内类似ChatGPT的大模型应用有哪些?发展情况如何了

第一部分&#xff1a;几个容易混淆的概念 很多人&#xff0c;包括很多粉丝的科技博主&#xff0c;经常把ChatGPT和预训练大模型混为一谈&#xff0c;因此有必要先做一个澄清。预训练大语言模型属于预训练大模型的一类&#xff0c;而ChatGPT、文心一言又是预训练大语言模型的一个…

如何使用 Apache 和 Nginx 创建临时和永久重定向

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。 简介 HTTP 重定向,或者 URL 重定向,是一种将一个域名或地址指向另一个的技术。重定向有许多用途,也有几种不同的重定向方式需要考虑。当一个站点需要将请求一个地址…

nginx的安装002

之前001讲述了nginxyum安装现在讲一下nginx如何本地离线安装 操作系统&#xff1a; CentOS Stream 9 操作步骤&#xff1a; 首先访问nginx官网&#xff0c;下载。 用wget命令下载&#xff0c; [rootlocalhost ~]# wget -c https://nginx.org/download/nginx-1.26.0.tar.gz …

React Hooks是如何保存的

React 函数式组件是没有状态的&#xff0c;需要 Hooks 进行状态的存储&#xff0c;那么状态是怎么存储的呢&#xff1f;Hooks是保存在 Fiber 树上的&#xff0c;多个状态是通过链表保存&#xff0c;本文将通过源代码分析 Hooks 的存储位置。 创建组件 首先我们在组件中添加两…

算法训练营day45

题目1&#xff1a;1049. 最后一块石头的重量 II - 力扣&#xff08;LeetCode&#xff09; 这道题主要的思路是把题目转换成分成两份&#xff0c;然后转换成一个重量为 sum / 2 的背包去装石头&#xff0c;尽可能将背包装的最大&#xff0c;那么最后最小省的石头就是 &#x…