'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;
}

According to this article, the compiler (almost) replaces f2() with a call to memset(), however, the compiler generates machine code from f1() that is almost identical to the above code. Because the compiler can assume that count and str do not point to the same int object (strict aliasing rule) in f2(), but cannot make such an assumption in f1() (C++ allow aliasing any pointer type with a char*).

Such aliasing problems can be avoided by dereferencing count in advance, as in f3(), or by using __restrict__, as in f4().

https://godbolt.org/z/fKTjcnW5f

The following functions are std::string/std::u8string version of the above functions:

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

My questions are:

  1. f5() and f6() are the same result as f1() and f2(), respectively. However, f7() and f8() do not have the same result as f3() and f4() (memset() was not used). Why?
  2. The compiler replaces f9() with a call to memset() (that does not happen with f8()). Why?

Tested with GCC 12.1 on x86_64, -std=c++20 -O3.



Solution 1:[1]

I created a simplified demo for the string case:

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;
}

The problem here is that the compiler cannot know whether writing to data_[i] does not change the value of data_. With the restricted s parameter, you tell the compiler that this cannot happen.

Live demo: https://godbolt.org/z/jjn9d3Mxe

This is not necessary for passing a pointer, since it is passed in the register, so it cannot be aliased with the pointed-to data. However, if this pointer is a global variable, the same problem occurs.

Live demo: https://godbolt.org/z/Y3nWvn6rW

Solution 2:[2]

My guess would be that your std::string implementation internally has a char *, since that is the default type for the string template. That char * is accessed in str[i] and being a char * can alias with any other pointer or reference, specifically with your str. So every time str[i] = 0 is evaluated the str object might change.

Not so when you restrict str.

Solution 3:[3]

When the first C STandard was written, character types in C and C++ were widely used for three purposes:

  1. Storing actual text characters.

  2. Holding small numbers.

  3. Accessing the raw bits in the storage underlying other types.

There is no particular reason why objects and pointers that are used for the first two purposes should have any kind of special aliasing rules, but the authors of the Standard wanted to make sure that aliasing rules wouldn't interfere with the use of character types for the third purpose.

A better way of accommodating the latter construct would have been to say that in contexts where a compiler can see that a T* is converted to U*, accesses performed via the U* will be recognized as possibly being performed "by" lvalues of type T. Applying such principles would eliminate the need for the "character type exception". Nonetheless, the fact that character types are sometimes used for the third purpose above has resulted in them having special rules which would not make sense if applied to other constructs that are only used for the first two purposes.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Peter Cordes
Solution 2 Goswin von Brederlow
Solution 3 supercat