【问题标题】:How to think about varieties of for loops in C++?如何考虑 C++ 中的各种 for 循环?
【发布时间】:2019-08-14 01:30:53
【问题描述】:

我的 IDE (CLion) 建议用 foreach 替换 for 循环,其中循环的元素是 char 类型值的地址(选项 2)。我很好奇以下几点:

  • 表达选项 2 中发生的事情的最佳方式是什么?我们是否在 plaintext 中遍历每个字符的内存位置?

  • 选项 2 和 3 有何不同?

  • 选项 3 是否在每次迭代时为新字符分配内存?

选项 1

void Cipher(std::string &plaintext, int key) {

  for (int i = 0; i < plaintext.length(); i++) {...}

}

选项 2

void Cipher(std::string &plaintext, int key) {

    for (char &letter : plaintext) {...}

}

选项 3

void Cipher(std::string &plaintext, int key) {

    for (char letter : plaintext) {...}

}

【问题讨论】:

  • 这三个版本都没有指针。
  • reference 是您需要阅读的术语。
  • 选项 1,没有。现在是 2019 年。选项 2 仅当 letter 的值要在循环中更改时。选项 3 又是可能的。一个有趣的选择是for (const auto &amp;letter : plaintext) {...}。将来更容易过渡到宽字符。
  • 我猜选项 3 与前两个不同,因为它在每次迭代时为新字符分配内存。这是正确的吗? 只是有点正确。该角色在自动存储中,因此几乎是免费的。如果编译器认为合适,它可能只是放在寄存器中。
  • 选项 1 应该用于您特别需要引用索引的情况(将其传递给另一个数据结构)。它并没有像其他人提到的那样完全过时。

标签: c++ pointers foreach function-pointers


【解决方案1】:

第 1 点

表达选项 2 中发生的事情的最佳方式是什么?我们是否在明文中遍历每个字符的内存位置?

它正在遍历plaintext 中的字符。这意味着它将遍历所有内存位置,但其他所有循环也是如此。 letter 是对plaintext 中一个字符的另一个名称的引用。不要将引用视为内存位置或指针(尽管可以在后台使用指针实现引用)。假设plaintext[0] 存在,将其视为letterplaintext[0] 是同一件事。没有letter,只有一个引用plaintext[0] 的标识符。当循环完成第一次迭代并进入第二次迭代(如果有)时,将有一个新的letter(不能引用不同的对象),它将是plaintext[1]

第 2 点

选项 2 和 3 有何不同?

如第 1 点所述,在选项 2 中,letterplaintext 中的字符之一。在选项 3 中,letter 是一个新变量,它是 plaintext 中的一个字符的副本。

第 3 点

选项 3 是否在每次迭代时为新字符分配内存?

是的,为循环的每次迭代分配了一个新的letter。但是,该字符是一个自动变量,根本不占用内存中的任何空间。它可能位于 CPU 寄存器中。它可能位于堆栈中,已经分配了存储空间,并且简单地更新了簿记以表明内存现在正在使用中。它可能漂浮在精灵尘埃中。无论发生什么,一旦优化编译器完成,您甚至可能无法检测到它。

【讨论】:

    【解决方案2】:

    应该不惜一切代价避免选项1!!!这里的问题是方法的输入(明文)是一个引用,因此字符串存在于方法的范围之外。这意味着编译器无法确定该变量的范围,因此无法确定执行优化是否安全(并非总是如此,但在这里)。

    https://godbolt.org/z/EBtVp7

    在这里实现一个愚蠢的方法(只需为每个字符添加 12)。您会注意到第一个版本的 ASM 看起来“不错”。它非常简单,非常小,很棒。但是,如果您将 1 切换为 0 并与第二种方法进行比较,您会注意到第二种方法在生成的 asm 数量方面呈爆炸式增长,但是当您仔细观察时,它并没有那么糟糕。

    看看第一个代码sn-p,我们可以在内循环的第一行看到这个:

    mov rcx, qword ptr [rdi]

    这有点糟糕。它实际上是在每次迭代时读取字符串“开始”指针(假设另一个线程*可能*调整字符串大小,因此更改字符串长度)

    但是,如果您查看第二种方法,它会使用 vpaddb 指令(使用 YMM 寄存器)生成一些展开的循环。这意味着它一次处理 32 个字符(不像第一种方法一次只能处理 1 个字符)

    如果你想开始让 option1 接近 option2 的性能,你需要做一些严峻的事情,比如:

    void Cipher(std::string &plaintext, int key) {
    
      if(!plaintext.empty())
      {
        char* ptr = &plaintext[0];
        for (int i = 0, length = plaintext.length(); i < length; i++) {
    
           ptr[i] += 12;
        }
      }
    }
    

    这个可怕的变化现在意味着编译器可以看到 ptr 和 length 变量在函数范围内没有变化,因此它现在能够向量化代码。 (不过,选项 2 和 3 仍然更有效!)

    Option3 不会在每次迭代时分配一个字符(它会将一个字符加载到一个通用寄存器中,或者将一组字符加载到一个 YMM 寄存器中)。在这种情况下,性能差异是没有实际意义的。如果要修改字符串,请使用 option2,如果字符串是只读的,请使用 option3。

    实现相同目的的旧替代方法是 std::for_each,但是它不再比基于范围的 for 循环更可取。

    【讨论】:

      猜你喜欢
      • 2022-11-28
      • 1970-01-01
      • 1970-01-01
      • 2021-10-31
      • 2015-07-13
      • 2023-03-13
      • 1970-01-01
      • 2020-05-03
      • 2019-06-01
      相关资源
      最近更新 更多