如何看待C++中的各种 for 循环?

How to think about varieties of for loops in C++?

本文关键字:for 循环 何看待 C++      更新时间:2023-10-16

我的IDE(CLion(建议将for循环替换为foreach,其中循环的元素是char类型的值的地址(选项2(。我对以下内容感到好奇:

  • 阐明选项 2 中发生的事情的最佳方式是什么?我们是否以明文形式循环访问每个字符的内存位置?

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

1 点

阐明选项 2 中发生的事情的最佳方式是什么?我们是否以明文形式循环访问每个字符的内存位置?

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

要点 2

选项 2 和 3 有何不同?

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

要点 3

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

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

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

https://godbolt.org/z/EBtVp7

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

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

mov rcx, qword ptr [rdi]

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

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

如果你想开始使选项1接近选项2的性能,你需要做一些严峻的事情,比如:

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和长度变量在函数范围内没有变化,因此它现在能够对代码进行矢量化。(选项 2 和 3 仍然更有效!

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

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