rust - Iterator::collect 是否分配与 String::with_capacity 相同的内存量?
问题描述
在 C++ 中,当连接一堆字符串(其中每个元素的大小大致已知)时,通常会预先分配内存以避免多次重新分配和移动:
std::vector<std::string> words;
constexpr size_t APPROX_SIZE = 20;
std::string phrase;
phrase.reserve((words.size() + 5) * APPROX_SIZE); // <-- avoid multiple allocations
for (const auto &w : words)
phrase.append(w);
同样,我在 Rust 中做了这个(这个块需要unicode-segmentation crate)
fn reverse(input: &str) -> String {
let mut result = String::with_capacity(input.len());
for gc in input.graphemes(true /*extended*/).rev() {
result.push_str(gc)
}
result
}
有人告诉我,惯用的做法是一个单一的表达方式
fn reverse(input: &str) -> String {
input
.graphemes(true /*extended*/)
.rev()
.collect::<Vec<&str>>()
.concat()
}
虽然我真的很喜欢它并想使用它,但从内存分配的角度来看,前者分配的块会比后者少吗?
我用它反汇编了它,cargo rustc --release -- --emit asm -C "llvm-args=-x86-asm-syntax=intel"
但它没有穿插源代码,所以我很茫然。
解决方案
您的原始代码很好,我不建议更改它。
原始版本分配一次: inside String::with_capacity
。
第二个版本至少分配两次:首先,它创建 a并通过ing s来Vec<&str>
增长它。然后,它计算所有s 的总大小并创建一个具有正确大小的新的。(此代码在 中的方法中。)这很糟糕,原因有以下几个:push
&str
&str
String
join_generic_copy
str.rs
- 显然,它不必要地分配。
- 字素簇可以任意大,因此中间体
Vec
不能提前有效地调整大小——它只是从大小 1 开始并从那里增长。 - 对于典型的字符串,它分配的空间比存储最终结果实际需要的空间多得多,因为
&str
通常大小为 16 字节,而 UTF-8 字形簇通常远小于此。 - 它浪费时间迭代中间体
Vec
以获得最终尺寸,您可以从原始尺寸中获取它&str
。
最重要的是,我什至不认为这个版本是惯用collect
的,因为它是一个临时版本Vec
,以便对其进行迭代,而不是collect
像您在早期版本的答案中那样仅仅使用原始迭代器。此版本修复了问题 #3 并使 #4 无关紧要,但不能令人满意地解决 #2:
input.graphemes(true).rev().collect()
collect
使用FromIterator
for String
,它将尝试使用size_hint
实现Iterator
for的下限Graphemes
。但是,正如我之前提到的,扩展的字形簇可以任意长,因此下限不能大于 1。更糟糕的是,&str
s 可能为空,因此FromIterator<&str>
forString
不知道结果的大小(以字节为单位) . 此代码只是创建一个空并重复String
调用它。push_str
需要明确的是,这还不错!String
有一个增长策略来保证分摊的 O(1) 插入,所以如果你有大部分不需要经常重新分配的小字符串,或者你不相信分配成本是一个瓶颈,collect::<String>()
那么在这里使用可能是合理的,如果你会发现它更具可读性和更容易推理。
让我们回到您的原始代码。
let mut result = String::with_capacity(input.len());
for gc in input.graphemes(true).rev() {
result.push_str(gc);
}
这是惯用的。collect
也是惯用的,但collect
基本上都是上面的,初始容量不太准确。由于collect
没有做你想做的事,所以自己编写代码并不习惯。
还有一个更简洁的迭代器版本,它仍然只进行一次分配。使用extend
方法,它是Extend<&str>
for的一部分String
:
fn reverse(input: &str) -> String {
let mut result = String::with_capacity(input.len());
result.extend(input.graphemes(true).rev());
result
}
我有一种extend
更好的模糊感觉,但这两种方式都是编写相同代码的完全惯用方式。你不应该重写它来使用collect
,除非你觉得它更好地表达了意图并且你不关心额外的分配。
有关的
推荐阅读
- python - Python try-except 块重新引发异常
- javascript - 如何更新子组件的状态?
- asp.net - ASP.NET MVC - 根据数据库中的值禁用按钮(WebGrid)
- watch - 真正的 IOS 设备和 Garmin 手表刺激器
- java - 如何为实体中的修剪字段制作自定义注释?
- wpf - 如何将命令绑定到 ItemsControl 的模板
- python - 用于发送带有证书、私有加密密钥和密码的请求的 Python 代码
- mysql - 为什么起始位置为零的子字符串不会在 SQL 中引发错误?
- javascript - 从另一个画布复制时如何缩放和翻译新画布?
- java - Java 格式字符串到 C#