Bootstrap

c++17 string_view

c++最为c程序员诟病的一点是,c++会在程序员不知道的地方申请和释放内存。c++库如stl等提供了良好的抽象,但是屏蔽底层操作的代价就是如果程序员不了解库的底层实现,就可能造成误用导致的性能损失,这里以std::string这个最常见的容器为例,从c++17的string_view展开,最后带来一个string特定场景下的使用优化。

1、std::string的内存申请

对c程序员来说,字符串从char*开始位置到'\0'结束,需要从字符串中切词通过移动指针指向子串的头部并提供新长度,c程序需要传入字符串的api具有如下特征:

void c_api(const char*, size_t len);

void func() {
    char buffer[] = "hello, world";
    c_api(buffer, 5);
}

在c程序中操作字符串的开销来自移动指针,可以忽略不计,然而在c++程序中,切子串通过substr实现,如下:

void cpp_api(const std::string&);

void func() {
    string s{"hello, world"};
    cpp_api(s.substr(0, 5));
}

这是c++中std::string的一个典型的使用方式,其问题在于substr有一次内存分配是潜在的性能杀手,通过对new打桩能够清晰的看到这一点:

void* operator new(size_t t) {
    cout << "malloc:" << t << endl;
    return malloc(t);
}

void cpp_api(const std::string&) {}

int main() {
    string s{"hello, world"};
    cpp_api(s.substr(0, 5));
    return 0;
}

运行结果如下:

malloc:37
malloc:30

可以看到除了原始string以外,substr重新构造了一个string类型对象,也有对应的申请内存开销

2、c++17 string_view

了解了std::string在上述模式下的性能开销来源后,我们来看c++17中为此问题提供的解决方案

void cpp_api(std::string_view) {}

void func() {
    string s{"hello, world"};
    string_view str_view{s.c_str(), s.size()};
    cpp_api(str_view.substr(0, 5));
}

此时再次执行new打桩的程序,可以发现只有原始string构造的一次分配内存开销,后续string_view的使用不会有内存分配开销,也就是说,在程序中需要对std::string进行只读切片时,使用string_view可以获取接近c指针移动的性能

3、针对特定场景写操作的优化

c++17 string_view解决了只读场景内存申请开销,在其他修改std::string的特定场景下,标准库尚未提供良好的解决方案,我们看这样一个例子

size_t c_api(char *s, size_t size) {}

void func() {
    string s;
    s.resize(1024); // 1024 is an estimate size enough to use
    size_t acture_size = c_api(s.data(), s.size());
    s.resize(acture_size);
}

这是一个调用第三方c库的典型模式,调用者先给std::string申请一块足够大的内存空间,然后调用c风格api,并根据返回的实际大小shrink到实际空间,这种场景的问题在于,std::string::resize()操作除了为string对象保留足够大小的空间外,还为这段空间执行了清零操作,类似calloc。由于马上就要覆盖string对象的内存空间,因此这个清零操作是不必要的,在一些网页编码解码的程序中,resize内的清零开销会对性能造成巨大的影响。基于特定编译器为std::string底层实现打桩,我们可以实现消除清零开销的目的,例如对gcc8我们可以这样操作:

template <typename C, typename T, typename A>
inline typename ::std::basic_string<C, T, A>::pointer resize_uninitialized(
        ::std::basic_string<C, T, A>& string,
        typename ::std::basic_string<C, T, A>::size_type size) {
    typedef ::std::basic_string<C, T, A> StringType;
    struct Rep {
        typename StringType::size_type length;
        typename StringType::size_type capacity;
        _Atomic_word refcount;
    };
    auto* data = (char*)(string.c_str());
    Rep* rep = (Rep*)(data) - 1;
    if (size > rep->capacity || rep->refcount > 0) {
        string.reserve(size);
        data = (char*)(string.c_str());
        rep = (Rep*)(data) - 1;
    }
    rep->length = size;
    data[size] = '\0';
    return data;
}

void func() {
    typedef std::basic_string<char, char_traits<char> > StringType;
    string s;
    auto *p = resize_uninitialized(s, 1024);
}

通过调用这样的resize_unitialized()就可以消除清零内存的开销

;