写点什么

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

作者:百度Geek说
  • 2022 年 4 月 12 日
  • 本文字数:4639 字

    阅读完需:约 15 分钟

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


前两期我们分享了日常工作中前端、移动端开发的相关问题,感兴趣的同学可以在文末推荐阅读跳转查看。本期我们分享三个议题: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 npimport paddleimport paddle.fluid as fluidfrom 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 npimport paddleimport paddle.fluid as fluidfrom 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)
复制代码


推荐阅读:


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


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


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


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


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


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

用户头像

百度Geek说

关注

百度官方技术账号 2021.01.22 加入

关注我们,带你了解更多百度技术干货。

评论

发布
暂无评论
百度程序员开发避坑指南(3)_前端_百度Geek说_InfoQ写作平台