首页 > 其他分享 >改进rust代码的35种具体方法-类型(二十)-避免过度优化的诱惑

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

时间:2024-06-01 17:30:48浏览次数:24  
标签:具体方法 type 代码 pub 35 let Rust data rust

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


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

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

数据结构和分配

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

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

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

  • 描述值类型的一个字节(存储在这里的type_code字段中)1
  • 一个字节描述以字节为单位的值长度(此处用于创建指定长度的切片)
  • 后跟值的指定字节数(存储在value字段中):
/// 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 data
        value: &input[2..2 + len],
    };
    Ok((tlv, &input[2 + len..]))
}

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

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

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

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()方法的任何数据的生命周期。这意味着一个简单的处理循环:

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);
    }
}

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

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

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

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);
    }
}

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

error[E0502]: cannot borrow `perma_buffer` as mutable because it is also
              borrowed 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数据结构被转换为拥有其内容:

#[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的借入检查规则:

  • RefCell:对于单线程代码中的内部可变性,给出常见的Rc<RefCell<T>>组合
  • Mutex:对于多线程代码中的内部可变性,给出commonArcArc<Mutex<T>>组合

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


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

标签:具体方法,type,代码,pub,35,let,Rust,data,rust
From: https://blog.csdn.net/qq_34841911/article/details/139375796

相关文章

  • Zero Trust Networks【3】
    一、WhatIsanAgent?二、HowtoExposeanAgent?Chapter3.Context-AwareAgents想象一下,你正处在一个有安全意识的组织中。每个员工都有一台经过高度认证的笔记本电脑来完成他们的工作。随着今天的工作和个人生活的融合,一些人还想在手机上查看他们的电子邮件和日历。在......
  • css35 CSS Navigation Bar
    https://www.w3schools.com/css/css_navbar.aspDemo:NavigationBars NavigationBarsHavingeasy-to-usenavigationisimportantforanywebsite.WithCSSyoucantransformboringHTMLmenusintogood-lookingnavigationbars.NavigationBar=ListofLi......
  • Q2 LeetCode35 搜索插入位置
    //有序查找,无重复元素,要求时间复杂度O(logn)//如果有目标元素则返回位置//如果没有目标元素,最后一次right位置后面就是该插入的位置第一次提交错误认为最后一次mid位置是插入的位置,其实最后一次right位置才是正确的插入位置(升序数组)1classSol......
  • 【计算机毕业设计】353微信小程序零食批发交易管理系统
    ......
  • Zero Trust【1】
    Chapter1.ZeroTrustFundamentals在一个网络监控无处不在的时代,我们发现很难信任任何人,而定义信任本身也同样困难。我们能相信,我们的互联网流量将是安全的,不会被窃听吗?当然不是!那你租用光纤的供应商呢?或者是昨天在你的数据中心处理电缆的合同技术人员?像爱德华·斯诺登和马克·......
  • 打卡信奥刷题(35)用Scratch图形化工具信奥P1664 [ 普及组] 每日打卡心情好
    每日打卡心情好题目背景在洛谷中,打卡不只是一个简单的鼠标点击动作,通过每天在洛谷打卡,可以清晰地记录下自己在洛谷学习的足迹。通过每天打卡,来不断地暗示自己:我又在洛谷学习了一天,进而帮助自己培养恒心、耐心、细心。此外,通过打卡,还可以获取经验值奖励,经验值的多少在一定......
  • 黑客团伙利用Python、Golang和Rust恶意软件袭击印国防部门;OpenAI揭秘,AI模型如何被用于
    巴黑客团伙利用Python、Golang和Rust恶意软件袭击印度国防部门!与巴基斯坦有联系的TransparentTribe组织已被确认与一系列新的攻击有关,这些攻击使用Python、Golang和Rust编写的跨平台恶意软件,针对印度政府、国防和航空航天部门。“这一系列活动从2023年底持续到2024年4月......
  • Atcoder ABC355 C~F
    C出题人太善良了,加强到\(10^5\)都没问题。考虑维护每条横线竖线两条对角线上被标记的点的个数,每次标记点后,判断是否有线上点全被标记。再考虑如何将点编号转为坐标,记编号为\(t\),推柿子:\[(x-1)\timesn+y=t\]\[nx+y=t+n\]\[x=\frac{t+n-y}{n}\]等同于找到\(y\)使得:\[n......
  • 普通人如何度过35岁中年危机
    普通人如何度过35岁中年危机,可以从以下几个方面进行详细的规划和应对:心理调适:接受现实:首先要接受自己正在经历中年危机的事实,并认识到这是衰老过程的正常部分。通过重新审视自己的价值观、兴趣爱好、职业发展等方面,进行深入思考。放松技巧:学习一些放松技巧,如深呼吸、肌肉......
  • ABC 354
    B-AtCoderJanken2本来想开\(\rmvector<pair<string,int>>\)的,但发现其实没有必要,整数部分只需求和即可。另外,多个字符串按字典序升序排序可以直接存\(\rmvector\)后\(\rmsort\)。#include<bits/stdc++.h>usingnamespacestd;usingi64=longlong;intmai......