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

问题描述 投票:0回答:1

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

#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 "";
好多了?

c++ clang compiler-optimization stdstring libc++
1个回答
49
投票

这是 libc++ 实现

std::string
时有意做出的决定。

首先,

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

在 libc++ 中,

std::string
的“短表示”包括:

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

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

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

接受

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

有关完整说明,请参阅下文:

正在初始化
""
/ 呼叫
string(const char*)

要了解发生了什么,让我们看一下 std::basic_string

libc++ 源代码:

// 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方法。

© www.soinside.com 2019 - 2024. All rights reserved.