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

cetgtptt  于 2023-07-01  发布在  其他
关注(0)|答案(1)|浏览(155)

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

#include <string>

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

std::string make_empty() {
    return "";
}

这编译为:(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

请注意,返回{}总共需要清零24个字节,而返回""只需要清零2个字节。为什么return "";会更好?

w41d8nur

w41d8nur1#

这是libcstd::string实现中有意做出的决定。
首先,std::string具有所谓的 * 小字符串优化(SSO)*,这意味着对于非常短(或空)的字符串,它将直接将其内容存储在容器中,而不是分配动态内存。这就是为什么我们在这两种情况下都看不到任何分配。
在libc
中,std::string的“简短表示”包括:
| 含义| Meaning |
| --| ------------ |
| “短标志”表示它是一个短字符串(零表示是)| "short flag" indicating that it is a short string (zero means yes) |
| 字符串的长度,不包括空结束符| length of the string, excluding null terminator |
| 填充字节以对齐字符串数据(basic_string<char>无)| padding bytes to align string data (none for basic_string<char> ) |
| 字符串数据,包括空终止符| string data, including null terminator |
对于空字符串,我们只需要存储两个字节的信息:

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

接受const char*的构造函数将只写入这两个字节,这是最低限度。默认构造函数 “不必要地”std::string包含的所有24个字节归零。**这可能总体上更好,因为它使编译器可以发出std::memset或其他SIMD并行方式来批量清零字符串数组。
有关完整的解释,请参见下文:

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

为了理解发生了什么,让我们看看libc++ source code for std::basic_string

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

最后调用__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
}

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

编译器优化后,将__is_long___size___data_的一个字节归零编译为:

mov word ptr [rdi], 0

初始化{}/调用string()

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

/* specifiers... */ basic_string() /* noexcept(...) */
  : /* leave memory indeterminate */ {
    // ...
    __default_init();
}

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

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

__rep()的值初始化会导致24个零字节,因为:

struct __rep {
    union {
        __long  __l; // first union member gets initialized,
        __short __s; // __long representation is 24 bytes large
        __raw   __r;
    };
};

结论

如果为了一致性,你想在任何地方都进行值初始化,不要让这一点妨碍你。不必要地将几个字节清零并不是您需要担心的大性能问题。
实际上,当初始化大量字符串时,它是有用的,因为可以使用std::memset,或者其他一些SIMD方式来清零内存。

相关问题