硬件 & 版本依据:ARM A53,ARMv7,ARM GCC 11.1.0

前言

这里针对前面的总结进行对应的分析,主要从三个点出发:

  • 内存频率
  • 内存管理
  • 数据分发

内存管理与锁通过 uftrace 对应用层调用进行追踪,统计对应的耗时与堆栈

分析

内存频率

通过统计视频帧达到的时间和数据大小可以得到相关数据分布:

不难看出大部分数据分布在 3800 - 25000 bytes 之间,而且与数据达到时间并无太大关系,这里将数据大小总体分为三段:。这里通过统计计算后得出:

  • 占比为 96%
  • 均值为 60921,占比 2%
  • 占比为 2%

总体来看,内存拷贝的大小大部分位于 之间,因此可以考虑将这个区间作为基本的数据测试大小,可以考虑采用并行的方式对数据进行复制

内存管理

Buffer 的空间申请位于分发者内部,当数据到达时申请空间然后通过指针计数的方式传递到对应的 FrameQueue 集合中。这就导致分发者每次有数据到达时必须先申请内存再进行复制,每次进行申请空间属实没这个必要,在空间分配激烈的情况下,反而会导致不好的效果。虽然 Buffer 管理的方式是通过智能指针的引用计数进行管理,但是前面提到了 FrameBufferMemory 是在析构函数中进行释放的,因此当引用计数为 0 时,需要在订阅者内花费额外的开销来进行释放,因此总体的流程如下(以最后一个订阅者为例,同时也是最坏情况):

flowchart LR
    subgraph "Dispatcher"
        DataArrived(数据达到)
        DataAllocated(申请空间)
        ChooseSet([选择 FrameQueue 集合])
        DataArrived --> DataAllocated --> ChooseSet
    end
    FrameQueue
    subgraph "Module Worker (Last)"
        WaitForData(等待数据达到)
        HandleData(等待数据达到)
        FreeData(数据释放)
        WaitForData --> HandleData --> FreeData
    end
    FrameQueue --> WaitForData
    ChooseSet --> FrameQueue

这里通过 uftrace 分别抓取分发者线程的构造对应 Buffer 与释放的时间(分配大小为数据中位数 60921),构造如下:

# DURATION     TID      FUNCTION
24.000 us [ 895] | malloc();
[ 895] | std::make_shared() {
[ 895] | std::allocator::allocator() {
0.500 us [ 895] | __gnu_cxx::new_allocator::new_allocator();
2.500 us [ 895] | } /* std::allocator::allocator */
0.500 us [ 895] | std::forward();
0.500 us [ 895] | std::forward();
0.500 us [ 895] | std::forward();
0.500 us [ 895] | std::forward();
0.333 us [ 895] | std::forward();
[ 895] | std::allocate_shared() {
0.333 us [ 895] | std::forward();
0.500 us [ 895] | std::forward();
0.500 us [ 895] | std::forward();
0.500 us [ 895] | std::forward();
0.500 us [ 895] | std::forward();
[ 895] | std::shared_ptr::shared_ptr() {
0.500 us [ 895] | std::forward();
0.500 us [ 895] | std::forward();
0.333 us [ 895] | std::forward();
0.500 us [ 895] | std::forward();
0.500 us [ 895] | std::forward();
[ 895] | std::__shared_ptr::__shared_ptr() {
0.500 us [ 895] | std::forward();
0.334 us [ 895] | std::forward();
0.334 us [ 895] | std::forward();
0.500 us [ 895] | std::forward();
0.500 us [ 895] | std::forward();
[ 895] | std::__shared_count::__shared_count() {
[ 895] | std::allocator::allocator() {
0.500 us [ 895] | __gnu_cxx::new_allocator::new_allocator();
2.500 us [ 895] | } /* std::allocator::allocator */
[ 895] | std::__allocate_guarded() {
[ 895] | std::allocator_traits::allocate() {
[ 895] | __gnu_cxx::new_allocator::allocate() {
0.500 us [ 895] | __gnu_cxx::new_allocator::_M_max_size();
1.667 us [ 895] | operator new();
6.334 us [ 895] | } /* __gnu_cxx::new_allocator::allocate */
8.333 us [ 895] | } /* std::allocator_traits::allocate */
[ 895] | std::__allocated_ptr::__allocated_ptr() {
0.500 us [ 895] | std::__addressof();
2.500 us [ 895] | } /* std::__allocated_ptr::__allocated_ptr */
13.333 us [ 895] | } /* std::__allocate_guarded */
[ 895] | std::__allocated_ptr::get() {
0.500 us [ 895] | std::__to_address();
2.333 us [ 895] | } /* std::__allocated_ptr::get */
[ 895] | std::allocator::allocator() {
0.500 us [ 895] | __gnu_cxx::new_allocator::new_allocator();
2.333 us [ 895] | } /* std::allocator::allocator */
0.500 us [ 895] | std::forward();
0.500 us [ 895] | std::forward();
0.500 us [ 895] | std::forward();
0.500 us [ 895] | std::forward();
0.500 us [ 895] | std::forward();
0.500 us [ 895] | operator new();
[ 895] | std::_Sp_counted_ptr_inplace::_Sp_counted_ptr_inplace() {
0.500 us [ 895] | std::_Sp_counted_base::_Sp_counted_base();
[ 895] | std::allocator::allocator() {
0.500 us [ 895] | __gnu_cxx::new_allocator::new_allocator();
2.333 us [ 895] | } /* std::allocator::allocator */
[ 895] | std::_Sp_counted_ptr_inplace::_Impl::_Impl() {
[ 895] | std::_Sp_ebo_helper::_Sp_ebo_helper() {
[ 895] | std::allocator::allocator() {
0.500 us [ 895] | __gnu_cxx::new_allocator::new_allocator();
2.500 us [ 895] | } /* std::allocator::allocator */
4.000 us [ 895] | } /* std::_Sp_ebo_helper::_Sp_ebo_helper */
5.667 us [ 895] | } /* std::_Sp_counted_ptr_inplace::_Impl::_Impl */
[ 895] | std::allocator::~allocator() {
0.500 us [ 895] | __gnu_cxx::new_allocator::~new_allocator();
2.333 us [ 895] | } /* std::allocator::~allocator */
[ 895] | std::_Sp_counted_ptr_inplace::_M_ptr() {
[ 895] | __gnu_cxx::__aligned_buffer::_M_ptr() {
0.500 us [ 895] | __gnu_cxx::__aligned_buffer::_M_addr();
2.833 us [ 895] | } /* __gnu_cxx::__aligned_buffer::_M_ptr */
4.334 us [ 895] | } /* std::_Sp_counted_ptr_inplace::_M_ptr */
0.500 us [ 895] | std::forward();
0.500 us [ 895] | std::forward();
0.500 us [ 895] | std::forward();
0.500 us [ 895] | std::forward();
0.334 us [ 895] | std::forward();
[ 895] | std::allocator_traits::construct() {
0.333 us [ 895] | std::forward();
0.500 us [ 895] | std::forward();
0.500 us [ 895] | std::forward();
0.334 us [ 895] | std::forward();
0.500 us [ 895] | std::forward();
[ 895] | __gnu_cxx::new_allocator::construct() {
0.500 us [ 895] | std::forward();
0.500 us [ 895] | std::forward();
0.333 us [ 895] | std::forward();
0.334 us [ 895] | std::forward();
0.333 us [ 895] | std::forward();
0.500 us [ 895] | operator new();
[ 895] | FrameBufferMemory::FrameBufferMemory() {
0.333 us [ 895] | FrameBuffer::FrameBuffer();
[ 895] | std::mutex::mutex() {
0.500 us [ 895] | std::__mutex_base::__mutex_base();
2.000 us [ 895] | } /* std::mutex::mutex */
4.666 us [ 895] | } /* FrameBufferMemory::FrameBufferMemory */
13.833 us [ 895] | } /* __gnu_cxx::new_allocator::construct */
22.167 us [ 895] | } /* std::allocator_traits::construct */
67.667 us [ 895] | } /* std::_Sp_counted_ptr_inplace::_Sp_counted_ptr_inplace */
[ 895] | std::allocator::~allocator() {
0.333 us [ 895] | __gnu_cxx::new_allocator::~new_allocator();
1.667 us [ 895] | } /* std::allocator::~allocator */
0.500 us [ 895] | std::__allocated_ptr::operator=();
[ 895] | std::_Sp_counted_ptr_inplace::_M_ptr() {
[ 895] | __gnu_cxx::__aligned_buffer::_M_ptr() {
0.500 us [ 895] | __gnu_cxx::__aligned_buffer::_M_addr();
1.834 us [ 895] | } /* __gnu_cxx::__aligned_buffer::_M_ptr */
3.000 us [ 895] | } /* std::_Sp_counted_ptr_inplace::_M_ptr */
0.500 us [ 895] | std::__allocated_ptr::~__allocated_ptr();
[ 895] | std::allocator::~allocator() {
0.333 us [ 895] | __gnu_cxx::new_allocator::~new_allocator();
1.834 us [ 895] | } /* std::allocator::~allocator */
115.167 us [ 895] | } /* std::__shared_count::__shared_count */
0.333 us [ 895] | std::__shared_ptr::_M_enable_shared_from_this_with();
138.334 us [ 895] | } /* std::__shared_ptr::__shared_ptr */
147.833 us [ 895] | } /* std::shared_ptr::shared_ptr */
157.333 us [ 895] | } /* std::allocate_shared */
[ 895] | std::allocator::~allocator() {
0.500 us [ 895] | __gnu_cxx::new_allocator::~new_allocator();
1.834 us [ 895] | } /* std::allocator::~allocator */
172.667 us [ 895] | } /* std::make_shared */

析构释放如下:

# DURATION     TID      FUNCTION
[ 885] | std::shared_ptr::~shared_ptr() {
[ 885] | std::__shared_ptr::~__shared_ptr() {
[ 885] | std::__shared_count::~__shared_count() {
[ 885] | std::_Sp_counted_base::_M_release() {
[ 885] | __gnu_cxx::__exchange_and_add_dispatch() {
0.500 us [ 885] | __gnu_cxx::__is_single_threaded();
0.333 us [ 885] | __gnu_cxx::__exchange_and_add();
3.500 us [ 885] | } /* __gnu_cxx::__exchange_and_add_dispatch */
[ 885] | std::_Sp_counted_ptr_inplace::_M_dispose() {
[ 885] | std::_Sp_counted_ptr_inplace::_Impl::_M_alloc() {
0.333 us [ 885] | std::_Sp_ebo_helper::_S_get();
2.167 us [ 885] | } /* std::_Sp_counted_ptr_inplace::_Impl::_M_alloc */
[ 885] | std::_Sp_counted_ptr_inplace::_M_ptr() {
[ 885] | __gnu_cxx::__aligned_buffer::_M_ptr() {
0.334 us [ 885] | __gnu_cxx::__aligned_buffer::_M_addr();
2.167 us [ 885] | } /* __gnu_cxx::__aligned_buffer::_M_ptr */
3.334 us [ 885] | } /* std::_Sp_counted_ptr_inplace::_M_ptr */
[ 885] | std::allocator_traits::destroy() {
[ 885] | __gnu_cxx::new_allocator::destroy() {
[ 885] | FrameBufferMemory::~FrameBufferMemory() {
1.667 us [ 885] | free();
0.500 us [ 885] | FrameBuffer::~FrameBuffer();
5.500 us [ 885] | } /* FrameBufferMemory::~FrameBufferMemory */
7.334 us [ 885] | } /* __gnu_cxx::new_allocator::destroy */
8.500 us [ 885] | } /* std::allocator_traits::destroy */
17.166 us [ 885] | } /* std::_Sp_counted_ptr_inplace::_M_dispose */
[ 885] | __gnu_cxx::__exchange_and_add_dispatch() {
0.333 us [ 885] | __gnu_cxx::__is_single_threaded();
0.333 us [ 885] | __gnu_cxx::__exchange_and_add();
3.167 us [ 885] | } /* __gnu_cxx::__exchange_and_add_dispatch */
[ 885] | std::_Sp_counted_ptr_inplace::_M_destroy() {
[ 885] | std::_Sp_counted_ptr_inplace::_Impl::_M_alloc() {
0.334 us [ 885] | std::_Sp_ebo_helper::_S_get();
1.834 us [ 885] | } /* std::_Sp_counted_ptr_inplace::_Impl::_M_alloc */
[ 885] | std::allocator::allocator() {
0.334 us [ 885] | __gnu_cxx::new_allocator::new_allocator();
2.000 us [ 885] | } /* std::allocator::allocator */
[ 885] | std::__allocated_ptr::__allocated_ptr() {
0.333 us [ 885] | std::__addressof();
2.000 us [ 885] | } /* std::__allocated_ptr::__allocated_ptr */
[ 885] | std::_Sp_counted_ptr_inplace::~_Sp_counted_ptr_inplace() {
[ 885] | std::_Sp_counted_ptr_inplace::_Impl::~_Impl() {
[ 885] | std::_Sp_ebo_helper::~_Sp_ebo_helper() {
[ 885] | std::allocator::~allocator() {
0.500 us [ 885] | __gnu_cxx::new_allocator::~new_allocator();
2.500 us [ 885] | } /* std::allocator::~allocator */
3.833 us [ 885] | } /* std::_Sp_ebo_helper::~_Sp_ebo_helper */
5.167 us [ 885] | } /* std::_Sp_counted_ptr_inplace::_Impl::~_Impl */
0.333 us [ 885] | std::_Sp_counted_base::~_Sp_counted_base();
8.000 us [ 885] | } /* std::_Sp_counted_ptr_inplace::~_Sp_counted_ptr_inplace */
[ 885] | std::__allocated_ptr::~__allocated_ptr() {
[ 885] | std::allocator_traits::deallocate() {
[ 885] | __gnu_cxx::new_allocator::deallocate() {
1.333 us [ 885] | operator delete();
3.334 us [ 885] | } /* __gnu_cxx::new_allocator::deallocate */
4.500 us [ 885] | } /* std::allocator_traits::deallocate */
5.833 us [ 885] | } /* std::__allocated_ptr::~__allocated_ptr */
[ 885] | std::allocator::~allocator() {
0.334 us [ 885] | __gnu_cxx::new_allocator::~new_allocator();
1.833 us [ 885] | } /* std::allocator::~allocator */
27.333 us [ 885] | } /* std::_Sp_counted_ptr_inplace::_M_destroy */
55.500 us [ 885] | } /* std::_Sp_counted_base::_M_release */
57.000 us [ 885] | } /* std::__shared_count::~__shared_count */
58.000 us [ 885] | } /* std::__shared_ptr::~__shared_ptr */
59.333 us [ 885] | } /* std::shared_ptr::~shared_ptr */

不难看到,malloc 也是耗时点之一,同时智能指针的申请对象的空间也在一定程度上消耗了时间。在最后一个订阅者通过析构释放归还给内存空间,虽然内部调用 free 的时间很短暂,但是完整走完智能指针的析构函数会占很大的时间

这里主要针对 FrameBufferMemory::readData() 锁进行分析,分别记录有无锁下的耗时:

# 有锁
# DURATION TID FUNCTION
[ 676] | FrameBufferMemory::readData() {
90.833 us [ 676] | memcpy();
117.833 us [ 676] | } /* FrameBufferMemory::readData */
|
[ 675] | FrameBufferMemory::readData() {
72.333 us [ 675] | memcpy();
100.333 us [ 675] | } /* FrameBufferMemory::readData */
|
[ 674] | FrameBufferMemory::readData() {
76.166 us [ 674] | memcpy();
94.334 us [ 674] | } /* FrameBufferMemory::readData */
|
[ 677] | FrameBufferMemory::readData() {
16.834 us [ 677] | memcpy();
45.167 us [ 677] | } /* FrameBufferMemory::readData */
|
[ 673] | FrameBufferMemory::readData() {
73.833 us [ 673] | memcpy();
93.166 us [ 673] | } /* FrameBufferMemory::readData */
|
[ 672] | FrameBufferMemory::readData() {
71.334 us [ 672] | memcpy();
90.834 us [ 672] | } /* FrameBufferMemory::readData */
|
[ 671] | FrameBufferMemory::readData() {
77.834 us [ 671] | memcpy();
96.334 us [ 671] | } /* FrameBufferMemory::readData */
|
[ 678] | FrameBufferMemory::readData() {
17.500 us [ 678] | memcpy();
149.000 us [ 678] | } /* FrameBufferMemory::readData */
|
[ 670] | FrameBufferMemory::readData() {
88.166 us [ 670] | memcpy();
108.000 us [ 670] | } /* FrameBufferMemory::readData */
|
[ 669] | FrameBufferMemory::readData() {
103.667 us [ 669] | memcpy();
119.000 us [ 669] | } /* FrameBufferMemory::readData */
# 无锁
# DURATION TID FUNCTION
[ 693] | FrameBufferMemory::readData() {
93.333 us [ 693] | memcpy();
106.834 us [ 693] | } /* FrameBufferMemory::readData */
|
[ 692] | FrameBufferMemory::readData() {
72.667 us [ 692] | memcpy();
74.666 us [ 692] | } /* FrameBufferMemory::readData */
|
[ 691] | FrameBufferMemory::readData() {
69.834 us [ 691] | memcpy();
71.666 us [ 691] | } /* FrameBufferMemory::readData */
|
[ 694] | FrameBufferMemory::readData() {
17.000 us [ 694] | memcpy();
18.833 us [ 694] | } /* FrameBufferMemory::readData */
|
[ 690] | FrameBufferMemory::readData() {
68.500 us [ 690] | memcpy();
70.500 us [ 690] | } /* FrameBufferMemory::readData */
|
[ 695] | FrameBufferMemory::readData() {
17.167 us [ 695] | memcpy();
19.167 us [ 695] | } /* FrameBufferMemory::readData */
|
[ 689] | FrameBufferMemory::readData() {
72.500 us [ 689] | memcpy();
74.333 us [ 689] | } /* FrameBufferMemory::readData */
|
[ 687] | FrameBufferMemory::readData() {
69.667 us [ 687] | memcpy();
71.500 us [ 687] | } /* FrameBufferMemory::readData */
|
[ 686] | FrameBufferMemory::readData() {
95.500 us [ 686] | memcpy();
97.500 us [ 686] | } /* FrameBufferMemory::readData */
|
[ 685] | FrameBufferMemory::readData() {
72.833 us [ 685] | memcpy();
74.834 us [ 685] | } /* FrameBufferMemory::readData */

计算得出两者的操作耗时均值:

memcpy 平均耗时 总体平均耗时
有锁操作
无锁操作

不难看出,无锁的情况下确实更加优异,总体耗时下降了 40% ,由于锁的竞争减小,使得多线程任务可以同时进行,由 A53 为 4 核,在不考虑内核调度的情况下,那么 3 个订阅者线程和 1 分发者线程得以同时进行

数据分发

分发者的工作线程采用的 std::set 的数组对 FrameQueue 进行托管,虽然工程中数组的长度并没有很多,但是订阅者的数量就不一定会少于数组的长度,因此分发者分配数据到对应的订阅者最坏的情况为 。考虑到 FrameBufferMemory 通过智能指针来实现了引用计数以及 FrameQueue 锁的竞争相对较少,因此分发者最主要的耗时存在于循环遍历找到对应的 FrameQueue 以及等待其将数据存放/覆盖到缓冲队列中

总结

通过上面的大致分析,可以得出一些改进的方向:

  1. 采用并行操作进行复制:通过 SIMD 指令进行并行复制,进一步减低复制所带来的耗时
  2. 改进引用计数:不通过智能指针来实现引用计数,从而避免内存的释放出现在订阅者的工作线程中
  3. 改进内存管理:分发者工作线程应无需关注内存的申请,同时既然是限定长度的缓存,那么可以考虑使用定长的内存池进行管理,从而避免频繁申请释放内存所带来的性能消耗
  4. 改进架构:原本的设计是由硬件实现的类进行订阅者的注册与分发,导致工作线程需要依次抢夺对应集合中所有队列的锁,这样导致了分发者必须进行等待,增大了分发者线程的负载