为什么将字符串初始化为“”比默认构造函数更有效?

Jan*_*tke 49 c++ clang stdstring compiler-optimization libc++

一般来说,默认构造函数应该是创建空容器的最快方法。这就是为什么我惊讶地发现它比初始化为空字符串文字更糟糕:

#include <string>

std::string make_default() {
    return {};
}

std::string make_empty() {
    return "";
}
Run Code Online (Sandbox Code Playgroud)

编译为:(clang 16,libc++)

make_default():
        mov     rax, rdi
        xorps   xmm0, xmm0
        movups  xmmword ptr [rdi], xmm0
        mov     qword ptr [rdi + 16], 0
        ret
make_empty():
        mov     rax, rdi
        mov     word ptr [rdi], 0
        ret
Run Code Online (Sandbox Code Playgroud)

请参阅编译器资源管理器中的实时示例

请注意,返回{}总共将 24 个字节归零,但返回""仅将 2 个字节归零。怎么会return "";好很多呢?

Jan*_*tke 53

这是 libc++ 实现std::string.

首先,std::string有所谓的小字符串优化(SSO),这意味着对于非常短(或空)的字符串,它将直接将其内容存储在容器内部,而不是分配动态内存。这就是为什么我们在这两种情况下都看不到任何分配。

在 libc++ 中,a 的“短表示”std::string包括:

尺寸(x86_64) 意义
1 位 “短标志”表示它是一个短字符串(零表示是)
7位 字符串的长度,不包括空终止符
0 字节 填充字节以对齐字符串数据(对于 没有填充basic_string<char>
23字节 字符串数据,包括空终止符

对于空字符串,我们只需要存储两个字节的信息:

  • 一个零字节用于“短标志”和长度
  • 一个零字节作为空终止符

接受 a 的构造函数const char*只会写入这两个字节,这是最少的。默认构造函数“不必要地”将包含的所有 24 个字节清零std::string不过,总体而言,这可能更好,因为它使编译器可以发出std::memset或其他 SIMD 并行方式将字符串数组批量归零。

完整的解释请参见下文:

初始化""/调用string(const char*)

要了解发生了什么,让我们看一下libc++ 源代码std::basic_string

// constraints...
/* specifiers... */ basic_string(const _CharT* __s)
  : /* leave memory indeterminate */ {
    // assert that __s != nullptr
    __init(__s, traits_type::length(__s));
    // ...
  }
Run Code Online (Sandbox Code Playgroud)

这最终会调用__init(__s, 0),其中0是从 获得的字符串长度std::char_traits<char>

// template head etc...
void basic_string</* ... */>::__init(const value_type* __s, size_type __sz)
{
    // length and constexpr checks
    pointer __p;
    if (__fits_in_sso(__sz))
    {
        __set_short_size(__sz); // set size to zero, first byte
        __p = __get_short_pointer();
    }
    else
    {
        // not entered
    }
    traits_type::copy(std::__to_address(__p), __s, __sz); // copy string, nothing happens
    traits_type::assign(__p[__sz], value_type()); // add null terminator
}
Run Code Online (Sandbox Code Playgroud)

__set_short_size最终将只写入一个字节,因为字符串的简短表示是:

struct __short
{
    struct _LIBCPP_PACKED {
        unsigned char __is_long_ : 1; // set to zero when active
        unsigned char __size_ : 7;    // set to zero for empty string
    };
    char __padding_[sizeof(value_type) - 1]; // zero size array
    value_type __data_[__min_cap]; // null terminator goes here
};
Run Code Online (Sandbox Code Playgroud)

编译器优化后,将__is_long___size_和 1 个字节归零,__data_编译结果为:

mov word ptr [rdi], 0
Run Code Online (Sandbox Code Playgroud)

初始化{}/调用string()

相比之下,默认构造函数更加浪费:

/* specifiers... */ basic_string() /* noexcept(...) */
  : /* leave memory indeterminate */ {
    // ...
    __default_init();
}
Run Code Online (Sandbox Code Playgroud)

这最终会调用__default_init(),它会执行以下操作:

/* specifiers... */ void __default_init() {
    __r_.first() = __rep(); // set representation to value-initialized __rep
    // constexpr-only stuff...
}
Run Code Online (Sandbox Code Playgroud)

a 的值初始化__rep()会产生 24 个零字节,因为:

struct __rep {
    union {
        __long  __l; // first union member gets initialized,
        __short __s; // __long representation is 24 bytes large
        __raw   __r;
    };
};
Run Code Online (Sandbox Code Playgroud)

结论

如果您为了一致性而想在各处进行值初始化,请不要因此而阻止您这样做。不必要地清零一些字节并不是您需要担心的大性能问题。

事实上,在初始化大量字符串时它很有帮助,因为std::memset可以使用或其他一些 SIMD 方式将内存清零。

  • 另一个实验 https://godbolt.org/z/rjzTvvrPa 似乎证明了您的观点,例如在初始化 `std::vector&lt;std::string&gt;` 时,默认构造函数如何更有效。 (4认同)