如何看待 C++ 中的各种 for 循环?
How to think about varieties of for loops in C++?
My 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) {...}
}
应该不惜一切代价避免选项 1!!!这里的问题是方法的输入(纯文本)是一个引用,因此字符串存在于方法的范围之外。这意味着编译器无法确定该变量的范围,因此无法确定执行优化是否安全(并非总是如此,但在这里是这样)。
在这里实现一个愚蠢的方法(只是向每个字符添加 12)。您会注意到第一个版本的 ASM 看起来 'nice'。它非常简单,非常小,很棒。但是,如果您将 1 切换为 0 并与第二种方法进行比较,您会注意到第二种方法在生成的 asm 数量方面呈爆炸式增长,但是当您仔细观察时并没有那么糟糕。
看一下第一个代码片段,我们可以在内循环的第一行中看到:
mov rcx, qword ptr [rdi]
这有点糟透了。它实际上是在每次迭代时读取字符串 'begin' 指针 (假设另一个线程 *可能* 调整字符串的大小,从而改变字符串的长度)。
如果您查看第二种方法,它会使用 vpaddb 指令(使用 YMM 寄存器)生成一些展开的循环。这意味着它一次处理 32 个字符 (不同于第一种方法,它一次只能处理一个字符)。
如果您想开始让选项 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 和 length 变量在函数范围内没有变化,因此它现在可以向量化代码。 (虽然选项 2 和 3 仍然更有效!)
选项 3 不会在每次迭代时分配一个字符 (它将一个字符加载到通用寄存器中,或者将一组字符加载到 YMM 寄存器中)。在这种情况下,性能差异是没有意义的。如果要修改字符串,请使用选项 2,如果字符串是只读的,请使用选项 3。
实现相同功能的较旧替代方法是 std::for_each,但是它不再优于基于范围的 for 循环。
点 1
What is the best way to articulate what's happening in Option 2? Do we iterate through the memory location of each character in plaintext?
正在遍历 plaintext
中的字符。这意味着它将遍历所有内存位置,但其他每个循环也会如此。 letter
是对 plaintext
中字符的引用,另一个名称。不要将引用视为内存位置或指针(尽管引用可能在幕后用指针实现)。将其视为 letter
和 plaintext[0]
是同一件事,假设 plaintext[0]
存在。没有 letter
,只有一个指向 plaintext[0]
的标识符。当循环完成第一次迭代并进入第二次迭代(如果它进入)时,将有一个新的 letter
(引用不能引用不同的对象)并且它将是 plaintext[1]
.
第 2 点
How do Options 2 and 3 differ?
如第 1 点所述,在选项 2 中,letter
是 plaintext
中的字符之一。在选项 3 中,letter
是一个新变量,它是 plaintext
.
中字符之一的副本
第 3 点
Does Option 3 allocate memory for a new char upon each iteration?
是的,为循环的每次迭代分配了一个新的 letter
。但是,该字符是一个自动变量,根本不占用内存中的任何 space 。它可能位于 CPU 寄存器中。它可能位于堆栈中,存储已经分配,簿记只是更新显示内存正在使用中。它可能漂浮在精灵尘埃中。无论发生什么,一旦优化编译器完成它,您甚至可能无法检测到它。
My 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) {...}
}
应该不惜一切代价避免选项 1!!!这里的问题是方法的输入(纯文本)是一个引用,因此字符串存在于方法的范围之外。这意味着编译器无法确定该变量的范围,因此无法确定执行优化是否安全(并非总是如此,但在这里是这样)。
在这里实现一个愚蠢的方法(只是向每个字符添加 12)。您会注意到第一个版本的 ASM 看起来 'nice'。它非常简单,非常小,很棒。但是,如果您将 1 切换为 0 并与第二种方法进行比较,您会注意到第二种方法在生成的 asm 数量方面呈爆炸式增长,但是当您仔细观察时并没有那么糟糕。
看一下第一个代码片段,我们可以在内循环的第一行中看到:
mov rcx, qword ptr [rdi]
这有点糟透了。它实际上是在每次迭代时读取字符串 'begin' 指针 (假设另一个线程 *可能* 调整字符串的大小,从而改变字符串的长度)。
如果您查看第二种方法,它会使用 vpaddb 指令(使用 YMM 寄存器)生成一些展开的循环。这意味着它一次处理 32 个字符 (不同于第一种方法,它一次只能处理一个字符)。
如果您想开始让选项 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 和 length 变量在函数范围内没有变化,因此它现在可以向量化代码。 (虽然选项 2 和 3 仍然更有效!)
选项 3 不会在每次迭代时分配一个字符 (它将一个字符加载到通用寄存器中,或者将一组字符加载到 YMM 寄存器中)。在这种情况下,性能差异是没有意义的。如果要修改字符串,请使用选项 2,如果字符串是只读的,请使用选项 3。
实现相同功能的较旧替代方法是 std::for_each,但是它不再优于基于范围的 for 循环。
点 1
What is the best way to articulate what's happening in Option 2? Do we iterate through the memory location of each character in plaintext?
正在遍历 plaintext
中的字符。这意味着它将遍历所有内存位置,但其他每个循环也会如此。 letter
是对 plaintext
中字符的引用,另一个名称。不要将引用视为内存位置或指针(尽管引用可能在幕后用指针实现)。将其视为 letter
和 plaintext[0]
是同一件事,假设 plaintext[0]
存在。没有 letter
,只有一个指向 plaintext[0]
的标识符。当循环完成第一次迭代并进入第二次迭代(如果它进入)时,将有一个新的 letter
(引用不能引用不同的对象)并且它将是 plaintext[1]
.
第 2 点
How do Options 2 and 3 differ?
如第 1 点所述,在选项 2 中,letter
是 plaintext
中的字符之一。在选项 3 中,letter
是一个新变量,它是 plaintext
.
第 3 点
Does Option 3 allocate memory for a new char upon each iteration?
是的,为循环的每次迭代分配了一个新的 letter
。但是,该字符是一个自动变量,根本不占用内存中的任何 space 。它可能位于 CPU 寄存器中。它可能位于堆栈中,存储已经分配,簿记只是更新显示内存正在使用中。它可能漂浮在精灵尘埃中。无论发生什么,一旦优化编译器完成它,您甚至可能无法检测到它。