首页 > 解决方案 > 如何考虑 C++ 中的各种 for 循环?

问题描述

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

选项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) {...}

}

标签: c++pointersforeachfunction-pointers

解决方案


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

https://godbolt.org/z/EBtVp7

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

看一下第一个代码片段,我们可以在内循环的第一行中看到:

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 循环更可取。


推荐阅读