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::&lt;Vec&lt;&amp;str&gt;&gt;().concat() @DanilaKiver 感谢您对size_hint 发表评论;不知道。内存分配请求/调用的数量会像第一种方法一样吗?我认为对于每个字素簇,由于对应的Vec::push,都会有一个分配,然后是concat 的最终分配。我问的原因不是特定于这个玩具示例,我试图了解第二种方法的工作原理。知道它会在更大的项目中有所帮助。 @legends2k,在重新阅读size_hint()实现后,我意识到它使用1作为边界,并且代码根据提示保留空间也依赖于 lower 界限(StringVec),所以感觉实际上 will 使用这种特定类型(@ 987654340@). 【参考方案1】:

您的原始代码很好,我不建议更改它。

原版分配一次:inside String::with_capacity

第二个版本分配至少两次:首先,它创建一个Vec&lt;&amp;str&gt; 并通过pushing &amp;strs 来增长它。然后,它计算所有&amp;strs 的总大小并创建一个具有正确大小的新String。 (此代码在the join_generic_copy method in str.rs 中。)这很糟糕有几个原因:

    显然,它不必要地分配。 字素簇可以任意大,因此中间的Vec 无法预先调整大小 - 它只是从大小 1 开始并从那里增长。 对于典型的字符串,它分配的空间比存储最终结果实际所需的空间多,因为&amp;str 通常是 16 字节大小,而 UTF-8 字形簇通常是远不止于此。 在中间 Vec 上进行迭代以获取最终大小是浪费时间,您可以从原始 &amp;str 中获取它。

最重要的是,我什至不认为这个版本是惯用的,因为它 collects 到一个临时的 Vec 以便对其进行迭代,而不是仅仅 collecting 原始迭代器,如你有你的答案的早期版本。此版本修复了问题 #3,使 #4 无关紧要,但不能令人满意地解决问题 #2:

input.graphemes(true).rev().collect()

collectFromIterator 用于String,这将是try to use 来自Iterator 实现的Graphemessize_hint 的下限。但是,正如我之前提到的,扩展的字形簇可以任意长,因此下限不能大于 1。更糟糕的是,&amp;strs 可能为空,所以 FromIterator&lt;&amp;str&gt;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&lt;&amp;str&gt; 的一部分,用于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 的地方在于它同时位于动词和受影响的宾语前面。您不必了解整行即可看到它有什么副作用:它extends result。使用collectfold 前面input,所以你会看到首先迭代的是什么,然后是迭代的形式,“最重要”的位——分配和填充字符串——被“埋”在表达式的结尾。 我会注意到,如果函数被多次使用,例如在循环中,提供一个带有&amp;mut String out 参数的版本可能是有益的。在某些情况下,这可能允许反复使用缓冲区。

以上是关于Iterator::collect 是不是分配与 String::with_capacity 相同的内存量?的主要内容,如果未能解决你的问题,请参考以下文章

我需要澄清一下我是不是应该(可以?)取消分配与视图相关的 UI 元素

为啥此代码段错误(在分配期间)与 pgi 而不是英特尔?

Java垃圾收集器与内存分配策略

主动领取与被动分配

分配变量时设置与选择?

JVM之内存分配与回收策略