Iterator::collect 是不是分配与 String::with_capacity 相同的内存量?
Posted
技术标签:
【中文标题】Iterator::collect 是不是分配与 String::with_capacity 相同的内存量?【英文标题】:Does Iterator::collect allocate the same amount of memory as String::with_capacity?Iterator::collect 是否分配与 String::with_capacity 相同的内存量? 【发布时间】:2020-02-24 22:04:04 【问题描述】:在 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"
反汇编了它,但它没有穿插源代码,所以我很茫然。
【问题讨论】:
“单一表达式”形式应该是折叠而不是使用集合Graphemes
的迭代器实现有 size_hint()
,String
在其FromIterator
实现中使用它来估计缓冲区大小,所以我认为不会有巨大的开销使用collect()
。
@DenysSéguret 你的意思是像.fold(String::with_capacity(input.len()), |result, gc| result + gc)
而不是.collect::<Vec<&str>>().concat()
?
@DanilaKiver 感谢您对size_hint
发表评论;不知道。内存分配请求/调用的数量会像第一种方法一样吗?我认为对于每个字素簇,由于对应的Vec::push
,都会有一个分配,然后是concat
的最终分配。我问的原因不是特定于这个玩具示例,我试图了解第二种方法的工作原理。知道它会在更大的项目中有所帮助。
@legends2k,在重新阅读size_hint()
实现后,我意识到它使用1
作为下边界,并且代码根据提示保留空间也依赖于 lower 界限(String
和 Vec
),所以感觉实际上 will 使用这种特定类型(@ 987654340@).
【参考方案1】:
您的原始代码很好,我不建议更改它。
原版分配一次:inside String::with_capacity
。
第二个版本分配至少两次:首先,它创建一个Vec<&str>
并通过push
ing &str
s 来增长它。然后,它计算所有&str
s 的总大小并创建一个具有正确大小的新String
。 (此代码在the join_generic_copy
method in str.rs
中。)这很糟糕有几个原因:
-
显然,它不必要地分配。
字素簇可以任意大,因此中间的
Vec
无法预先调整大小 - 它只是从大小 1 开始并从那里增长。
对于典型的字符串,它分配的空间比存储最终结果实际所需的空间多,因为&str
通常是 16 字节大小,而 UTF-8 字形簇通常是远不止于此。
在中间 Vec
上进行迭代以获取最终大小是浪费时间,您可以从原始 &str
中获取它。
最重要的是,我什至不认为这个版本是惯用的,因为它 collect
s 到一个临时的 Vec
以便对其进行迭代,而不是仅仅 collect
ing 原始迭代器,如你有你的答案的早期版本。此版本修复了问题 #3,使 #4 无关紧要,但不能令人满意地解决问题 #2:
input.graphemes(true).rev().collect()
collect
将FromIterator
用于String
,这将是try to use 来自Iterator
实现的Graphemes
的size_hint
的下限。但是,正如我之前提到的,扩展的字形簇可以任意长,因此下限不能大于 1。更糟糕的是,&str
s 可能为空,所以 FromIterator<&str>
为 String
不知道任何关于结果大小的内容(以字节为单位)。这段代码只是创建了一个空的String
并对其反复调用push_str
。
需要明确的是,这还不错! String
有一个增长策略,可以保证分期 O(1) 插入,所以如果你有大部分不需要经常重新分配的小字符串,或者你不相信分配成本是一个瓶颈,使用 @987654352如果您发现它更易读且更容易推理,这里的 @ 可能是合理的。
让我们回到你原来的代码。
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>
的一部分,用于String
:
fn reverse(input: &str) -> String
let mut result = String::with_capacity(input.len());
result.extend(input.graphemes(true).rev());
result
我有一种模糊的感觉,extend
更好,但这两种方式都是编写相同代码的完全惯用方式。你不应该重写它以使用collect
,除非你觉得它更好地表达了意图并且你不关心额外的分配。
相关
Efficiency of flattening and collecting slices【讨论】:
我是extend
版本的忠实粉丝,这就是我的选择。
我认为我喜欢extend
的地方在于它同时位于动词和受影响的宾语前面。您不必了解整行即可看到它有什么副作用:它extend
s result
。使用collect
或fold
前面input
,所以你会看到首先迭代的是什么,然后是迭代的形式,“最重要”的位——分配和填充字符串——被“埋”在表达式的结尾。
我会注意到,如果函数被多次使用,例如在循环中,提供一个带有&mut String
out 参数的版本可能是有益的。在某些情况下,这可能允许反复使用缓冲区。以上是关于Iterator::collect 是不是分配与 String::with_capacity 相同的内存量?的主要内容,如果未能解决你的问题,请参考以下文章