文先生的博客 求职,坐标深圳。(wenfh2020@126.com)

[stl 源码分析] std::list::size 时间复杂度

2021-04-09

项目在 Centos 上压测,很多性能问题都暴露了出来,没想到 std::list::size 接口,时间复杂度竟然是 O(N)。

看了 Centos 的 stl 源码,发现确实是循环遍历实现的,为啥每次都要循环计算大小呢?感觉这里是个坑啊。


1. 现象

功能测试完成后,发现 cpu 一直很高,所以就上火焰图,看到 std::list::size 占满了负载。

火焰图参考:如何生成火焰图🔥


2. 原因

g++ 低版本的坑,为了兼容某些功能,列表通过遍历节点获取列表大小。

出现问题的 g++ 版本是:4.8.5。

  • g++ 版本。
1
2
# g++ --version
g++ (GCC) 4.8.5 20150623 (Red Hat 4.8.5-44)
  • 测试用例。判断列表为空,谨慎使用 std::list::size(),最好使用 std::list::empty()。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <list>

int main() {
    std::list<int> lst;
    for (int i = 0; i < 10; i++) {
        lst.push_back(i);
    }

    if (!lst.empty()) {
        std::cout << "list is not empty 1.\n";
    }

    // 【警告】避免使用
    if (lst.size() != 0) {
        std::cout << "list is not empty 2.\n";
    }
    return 0;
}

3. 源码分析

std::list 通过遍历列表,获取列表大小。

3.1. 低版本 STL 源码

  • 源码调用逻辑。
1
2
3
4
5
|-- main
  |-- std::list::size
    |-- std::list::_M_node_count
      |-- std::distance
        |-- std::__distance
  • 源码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
template <typename _Tp, typename _Alloc = std::allocator<_Tp>>
    class list : protected _List_base<_Tp, _Alloc> {
    ...
    /**  Returns the number of elements in the %list.  */
    size_type size() const _GLIBCXX_NOEXCEPT { 
        return std::distance(begin(), end()); 
    }
    ...
}

template<typename _InputIterator>
inline typename iterator_traits<_InputIterator>::difference_type
distance(_InputIterator __first, _InputIterator __last) {
    // concept requirements -- taken care of in __distance
    return std::__distance(
        __first, __last, std::__iterator_category(__first));
}

template <typename _InputIterator>
inline typename iterator_traits<_InputIterator>::difference_type
__distance(_InputIterator __first, 
          _InputIterator __last, input_iterator_tag) {
    // concept requirements
    __glibcxx_function_requires(
        _InputIteratorConcept<_InputIterator>)

    /* 遍历列表获取 __n。*/
    typename iterator_traits<_InputIterator>::difference_type __n = 0;
    while (__first != __last) {
        ++__first;
        ++__n;
    }
    return __n;
}

3.2. 高版本 STL 源码

_List_node_header 结构添加了 _M_size 成员保存列表大小。

引入了 _GLIBCXX_USE_CXX11_ABI 宏,主要是为了处理 std::string 的 copy-on-write 问题和 std::list::size 时间复杂度问题。

  • g++ 版本。
1
2
# g++ --version
g++ (GCC) 9.3.1 20200408 (Red Hat 9.3.1-2)
  • 源码调用逻辑。
1
2
3
4
5
|-- main
  |-- std::list::size
    |-- std::list::_M_node_count
      |-- std::list::_M_get_size
        |-- return _M_impl._M_node._M_size;
  • 内部源码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 增加了 _M_size 列表大小成员
struct _List_node_header : public _List_node_base {
#if _GLIBCXX_USE_CXX11_ABI
    std::size_t _M_size;
#endif
}

template <typename _Tp, typename _Alloc = std::allocator<_Tp> >
class list : protected _List_base<_Tp, _Alloc> {
    //...
    size_type
    size() const _GLIBCXX_NOEXCEPT {
        return _M_node_count();
    }

#if _GLIBCXX_USE_CXX11_ABI
    static size_t
    _S_distance(const_iterator __first, const_iterator __last) {
        return std::distance(__first, __last);
    }

    // return the stored size
    size_t _M_node_count() const {
        return this->_M_get_size();
    }
#else
    // dummy implementations used when the size is not stored
    static size_t
    _S_distance(const_iterator, const_iterator) {
        return 0;
    }

    // count the number of nodes
    size_t _M_node_count() const {
        return std::distance(begin(), end());
    }
#endif

#if _GLIBCXX_USE_CXX11_ABI
    size_t _M_get_size() const {
        return _M_impl._M_node._M_size;
    }
#endif
};

作者公众号
微信公众号,干货持续更新~