百度程序员开发避坑指南(3)

原创
2022/04/12 15:45
阅读数 2.1K

图片

前两期我们分享了日常工作中前端、移动端开发的相关问题,感兴趣的同学可以在文末推荐阅读跳转查看。本期我们分享三个议题:golang对象池减少gc压力、FFmpeg中的并发控制、paddle的静态图和动态图,希望能为你的技术提升助力。

01golang对象池减少gc压力

sync.Pool是Golang 内置的对象池技术,可用于缓存临时对象,避免因频繁建立临时对象所带来的消耗以及对 GC 造成的压力。sync.Pool 缓存的对象随时可能被无通知的清除,因此不能将 sync.Pool 用于存储持久对象的场景。sync.Pool 不仅是并发安全的,而且通过引入atomic 包中的 CAS 操作实现了 lock free,通过更接近CPU和操作系统级别的原子操作,满足并发场景替代锁的需求。

1.1 使用

sync.Pool 在初始化的时候,需要用户提供一个对象的构造函数 New。用户使用 Get 来从对象池中获取对象,使用 Put 将对象归还给对象池。整个用法还是比较简单的。

1.2 原理

在 GMP 调度模型中,从线程维度来看,在 P 上的逻辑都是单线程执行的,这就为解决P上面协程并发提供了条件。sync.Pool 就是充分利用了 GMP 这一特点。对于同一个 sync.Pool ,每个 P 都有一个自己的本地对象池 poolLocal。每个P都会对应一个自身的本地对象池poolLocal,poolLocal就是存储P本地对象的内存池,每个poolLocal对应一个private和一个poolChain,private很简单是一个接口类型,比poolChain优先写也会被优先读。poolChain是指向若干个ringBuffer组成的链表,采用ringBuffer是因为环形结构方便内存复用,而且ringBuffer是一段连续内存,利于进行CPU Cache。

poolChain存放的是每个ringBuffer的head和tail,head 和 tail 并不是独立的两个变量,只有一个 uint64 的 headTail 变量。这是因为 headTail 变量将 head 和 tail 打包在了一起:其中高 32 位是 head 变量,低 32 位是 tail 变量,这个其实是个非常常见的 lock free 优化手段。对于一个 poolDequeue 来说,可能会被多个 P 同时访问,比如Get 函数中的对象窃取逻辑,这个时候就会带来并发问题。例如:当 ring buffer 空间仅剩一个的时候,即 head - tail = 1。如果多个 P 同时访问 ring buffer,在没有任何并发措施的情况下,两个 P 都可能会拿到对象,这肯定是不符合预期的。在不引入 Mutex 锁的前提下,sync.Pool 利用了 atomic 包中的 CAS 操作。两个 P 都可能会拿到对象,但在最终设置 headTail 的时候,只会有一个 P 调用 CAS 成功,另外一个 CAS 失败。

02FFmpeg中的并发控制

2.1 问题描述

最近业务需要,在一个探索性项目里,要进行视频的拼接与合成,由于原始的视频片段的格式、尺寸、码率等都各不相同,为了得到比较丝滑的拼接效果,需要首先进行视频尺寸的打齐、编码格式以及码率的统一。在对FFmepg命令进行了一番调研,并进行了一系列的转换实验(比如视频裁剪、视频填充、视频缩放等),均得到了符合预期的效果,但当把命令集成到实际业务场景里时,先后遭遇了内存被打爆进程被杀、CPU满负荷导致任务执行时间过长甚至失败,且进一步升级CPU配置问题并没有得到太大改善,于是开启了对FFmpeg命令的线程控制的调研。

2.2 FFmpeg线程控制

FFmpeg作为强大的多媒体处理工具,包含多个功能强大的lib库。FFmpeg处理多媒体文件流程如下:

其中关键的计算步骤为编码、解码以及其中数据修改,同时FFmpeg的线程控制也提供了三个参数进行线程控制,在FFmpeg文档里,对于线程控制相关参数说明如下:

-filter_threads nb_threads (global) 

Defines how many threads are used to process a filter pipeline. Each pipeline will produce a thread pool with this many threads available for parallel processing. The default is the number of available CPUs.

filter_threads实现对简单滤镜的线程控制,默认线程数为可用CPU核数

-filter_complex_threads nb_threads (global) 

Defines how many threads are used to process a filter_complex graph. Similar to filter_threads but used for -filter_complex graphs only. The default is the number of available CPUs.

filter_complex_threads实现对复杂滤镜的线程控制,默认线程数同样为可用CPU核数

threads integer (decoding/encoding,video) 

Set the number of threads to be used, in case the selected codec implementation supports multi-threading. 

Possible values: 

‘auto, 0’automatically select the number of threads to set 

Default value is ‘auto’.

threads实现对编解码器的线程控制,前提是使用的编解码器支持多线程并行,其默认线程数在文档里就一个automatically说明,翻遍全网也没找到对这个参数的具体说明,于是基于业务场景进行了相关数据实验。

使用time命令查看FFmpeg命令的耗时和CPU利用率相关参数,4核机器上试验情况如下:

-i  -filter_complex  -threads 1 -y   4.54s user 0.17s system 110% cpu 4.278 total
-i  -filter_complex  -threads 2 -y   4.61s user 0.29s system 189% cpu 2.581 total
-i  -filter_complex  -threads 4 -y   4.92s user 0.22s system 257% cpu 1.993 total
-i -filter_complex -threads 6 -y 4.73s user 0.21s system 302% cpu 1.634 total
-i -filter_complex -threads 8 -y 4.72s user 0.19s system 315% cpu 1.552 total
-i  -filter_complex  -y   4.72s user 0.22s system 306% cpu 1.614 total
-i  -filter_complex  -y -filter_complex_threads 1 -y   4.63s user 0.13s system 316% cpu 1.504 total
-i  -filter_complex  -y -filter_complex_threads 2 -y   4.62s user 0.20s system 304% cpu 1.583 total
-i  -filter_complex  -y -filter_complex_threads 4 -y   4.58s user 0.27s system 303% cpu 1.599 total

通过试验发现在不加线程控制情况下,对于我的裁剪+缩放+gblur尺寸打平操作来说,几乎没有并行空间,filter_complex_threads增加线程数徒增系统态耗时,对于整体耗时和CPU利用来说几乎没有增益。而对于编解码部分随着线程数的增多,CPU利用率增大且耗时降低,但是整体数据呈现出来的并非线性关系。而对于单条命令而言,线程数设置为2时基本是CPU消耗和耗时相对性价比比较高的配置。

2.3 总结

1. FFmepg作为计算密集型处理工具,对CPU有比较大的需求,且FFmpeg提供了三个并行控制参数分别进行不同类型命令的并发控制,但是具体命令是否可并发与本身的实现原理有关,需要具体问题具体分析;

2. 编解码作为FFmpeg视频处理里关键环节,相对也是比较耗时、耗CPU的环节,用好threads参数能够比较好地加速处理、控制CPU使用率

03paddle的静态图和动态图

静态图和动态图的概念

静态图:类比c++,先编译后运行。因此可以分为compile time和runtime两个阶段。在compiletime,需要预先定义完整的模型,paddle会生成一个programDesc,然后使用transplier对programDesc进行优化。在runtime,executor使用programDesc进行运行。

图片

动态图:类比python,没有编译阶段,所以不用预先定义模型。每写一行网络代码,即可同时获得对应计算结果。

优缺点对比:

静态图paddle一开始只支持静态图方式,所以相关的支持和文档比较多。在性能方面也较动态图好。但是调试起来会比较麻烦。
动态图:方便调试,可以动态调整模型结构。但是执行效率较低。

问题1:如何判断当前是静态图模式还是动态图模式

  • 静态图模式:程序中存在static模块使用,或者需要构建executor并使用executor.run(program)执行定义好的模型。

  • 动态图模式:程序中存在dygraph模块使用。在paddle2.0开始,默认开启动态图模式。

  • 注意:部分api仅支持静态图/动态图,如涉及variable取值等api,一般仅支持动态图。当出现带imperative/dygraph等报错时,需要确认是否在静态图模式中调用了动态图api。

import numpy as np
import paddle
import paddle.fluid as fluid
from paddle.fluid.dygraph.base import to_variable

print (paddle.__version__) # 2.1.1

#静态图模式
main_program = fluid.Program()
startup_program = fluid.Program()
paddle.enable_static()
with fluid.program_guard(main_program=main_program, startup_program=startup_program):
    data_x = np.ones([2, 2], np.float32)
    data_y = np.ones([2, 2], np.float32)
    # 静态图模式下,构建占位符
    x = fluid.layers.data(name='x', shape=[2], dtype='float32')
    y = fluid.layers.data(name='y', shape=[2], dtype='float32')
    x = fluid.layers.elementwise_add(x, y)
    print ('In static mode, after calling layers.data, x = ', x)
    # 这个时候无法打印出运行数值,输出In static mode, after calling layers.data, x =  var elementwise_add_0.tmp_0 : LOD_TENSOR.shape(-1, 2).dtype(float32).stop_gradient(False)
    place = fluid.CPUPlace()
    exe = fluid.Executor(place=place)
    exe.run(fluid.default_startup_program())
    data_after_run = exe.run(fetch_list=[x], feed={'x': data_x, 'y': data_y})
    print ('In static mode, data after run:', data_after_run)
    #In static mode, data after run: [array([[2., 2.],[2., 2.]], dtype=float32)]


# 动态图模式
with fluid.dygraph.guard():
    x = np.ones([2, 2], np.float32)
    y = np.ones([2, 2], np.float32)
    # 动态图模式下,将numpy的ndarray类型的数据转换为Variable类型
    x = fluid.dygraph.to_variable(x)
    y = fluid.dygraph.to_variable(y)
    print ('In DyGraph mode, after calling dygraph.to_variable, x = ', x)
    # In DyGraph mode, after calling dygraph.to_variable, x =  Tensor(shape=[2, 2], dtype=float32, place=CUDAPlace(0), stop_gradient=True,[[1., 1.],[1., 1.]])
    x = fluid.layers.elementwise_add(x,y)
    print ('In DyGraph mode, data after run:', x.numpy())
    #In DyGraph mode, data after run: [[2. 2.] [2. 2.]]

问题2:如何在静态图模式下调试

  • 一般使用fluid.layers.Print(),创建一个打印operator,对正在访问的tensor内容进行打印。

问题3:动态图如何转静态图

  • 基于动态图的优缺点,可以在模型开发阶段使用动态图模式,在训练及推理阶段使用静态图模式。

  • 在需要进行动静转化的函数上,使用 @paddle.jit.to_static 进行装饰。或者使用paddle.jit.to_static()函数对网络整体进行转化。

import numpy as np
import paddle
import paddle.fluid as fluid
from paddle.jit import to_static

class MyNet(paddle.nn.Layer):
    def __init__(self):
        super(MyNet, self).__init__()
        self.fc = fluid.dygraph.Linear(input_dim=4, output_dim=2, act="relu")

    @to_static
    def forward(self, x, y):
        x = fluid.dygraph.to_variable(x)
        x = self.fc(x)
        y = fluid.dygraph.to_variable(y)
        loss = fluid.layers.cross_entropy(input=x, label=y)
        return loss


net = MyNet()
x = np.ones([16, 4], np.float32)
y = np.ones([16, 1], np.int64)
net.eval()
out = net(x, y)

推荐阅读:

百度程序员开发避坑指南(移动端篇)

百度程序员开发避坑指南(前端篇)

百度工程师教你快速提升研发效率小技巧

百度一线工程师浅谈日新月异的云原生

【技术加油站】揭秘百度智能测试规模化落地

【技术加油站】浅谈百度智能测试的三个阶段

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
2 收藏
1
分享
返回顶部
顶部