为什么 char* 和 std::string& 的别名优化结果不同?

Why are the results of the optimization on aliasing different for char* and std::string&?

void f1(int* count, char* str) {
  for (int i = 0; i < *count; ++i) str[i] = 0;
}

void f2(int* count, char8_t* str) {
  for (int i = 0; i < *count; ++i) str[i] = 0;
}

void f3(int* count, char* str) {
  int n = *count;
  for (int i = 0; i < n; ++i) str[i] = 0;
}

void f4(int* __restrict__ count, char* str) { // GCC extension; clang also supports it
  for (int i = 0; i < *count; ++i) str[i] = 0;
}

根据this article, 编译器(几乎)将 f2() 替换为对 memset() 的调用, 然而,编译器从 f1() 生成的机器代码几乎与上述代码相同。 因为编译器可以假定 countstr 不指向同一个 int 对象(严格的别名规则)在 f2() , 但不能在 f1() 中做出这样的假设(C++ 允许使用 char* 别名任何指针类型)。

可以通过提前取消引用 count 来避免此类别名问题,如 f3() 或使用 __restrict__,如 f4().

https://godbolt.org/z/fKTjcnW5f

以下函数是上述函数的std::string/std::u8string版本:

void f5(int* count, std::string& str) {
  for (int i = 0; i < *count; ++i) str[i] = 0;
}

void f6(int* count, std::u8string& str) {
  for (int i = 0; i < *count; ++i) str[i] = 0;
}

void f7(int* count, std::string& str) {
  int n = *count;
  for (int i = 0; i < n; ++i) str[i] = 0;
}

void f8(int* __restrict__ count, std::string& str) {
  for (int i = 0; i < *count; ++i) str[i] = 0;
}

void f9(int* __restrict__ count, std::string& __restrict__ str) {
  for (int i = 0; i < *count; ++i) str[i] = 0;
}

https://godbolt.org/z/nsPdfhzoj

我的问题是:

  1. f5()f6() 分别与 f1()f2() 的结果相同。 但是,f7()f8()f3()f4() 的结果不同(未使用 memset())。为什么?
  2. 编译器将 f9() 替换为对 memset() 的调用(f8() 不会发生这种情况)。为什么?

已在 x86_64、-std=c++20 -O3.

上使用 GCC 12.1 进行测试

我的猜测是您的 std::string 实现内部有一个 char *,因为这是字符串模板的默认类型。 char * 在 str[i] 中访问并且作为 char * 可以与任何其他指针或引用别名,特别是与您的 str。所以每次 str[i] = 0 被评估时, str 对象可能会改变。

当你限制 str 时就不是这样了。

我为 string 案例创建了一个简化的演示:

class String {
    char* data_;
public:
    char& operator[](size_t i) { return data_[i]; }
};

void f(int n, String& s) {
    for (int i = 0; i < n; i++) s[i] = 0;
}

这里的问题是编译器不知道写入data_[i]是否不会改变data_的值。使用受限的 s 参数,您可以告诉编译器这不可能发生。

现场演示:https://godbolt.org/z/jjn9d3Mxe

这不是传递指针所必需的,因为它是在寄存器中传递的,所以它不能与pointed-to数据混淆。但是,如果这个指针是一个全局变量,也会出现同样的问题。

现场演示:https://godbolt.org/z/Y3nWvn6rW

编写第一个 C 标准时,C 和 C++ 中的字符类型被广泛用于三个目的:

  1. 存储实际文本字符。

  2. 持有小号。

  3. 访问存储在其他类型下的原始位。

用于前两个目的的对象和指针应该有任何类型的特殊别名规则没有特别的原因,但标准的作者想确保别名规则不会干扰将字符类型用于第三个目的。

适应后一种构造的更好方法是说,在编译器可以看到 T* 转换为 U* 的上下文中,通过 [=11= 执行的访问] 将被识别为可能由 T 类型的左值“执行”。应用这些原则将消除对“字符类型例外”的需要。尽管如此,字符类型有时用于上述第三个目的的事实导致它们具有特殊规则,如果应用于仅用于前两个目的的其他构造,这些规则将没有意义。