前两期,我们分享了日常工作中前端和移动开发相关的问题。有兴趣的同学可以在文末推荐阅读和跳转查看。本期分享三个话题:golang对象池减轻gc压力、FFmpeg中的并发控制、paddle的静态动态图形,希望能帮助大家提升技术。01golang对象池降低gc压力sync.Pool是Golang中内置的对象池技术,可以用来缓存临时对象,避免频繁创建临时对象带来的消耗和GC压力。sync.Pool缓存的对象可能随时被清除,恕不另行通知,因此不能使用sync.Pool来存储持久化对象。sync.Pool不仅是并发安全的,而且通过在atomic包中引入CAS操作实现了无锁。通过更接近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是连续的内存,有利于CPUCache。poolChain存储每个ringBuffer的头部和尾部。头和尾不是两个自变量。只有一个uint64headTail变量。这是因为headTail变量将头部和尾部打包在一起:高32位是头部变量,低32位是尾部变量。这其实是一种很常见的无锁优化方法。对于一个poolDequeue,可能同时被多个P访问,比如Get函数中的对象窃取逻辑,此时会造成并发问题。例如:当只剩下一个ringbuffer空间时,head-tail=1。如果多个P同时访问ringbuffer,没有任何并发??措施,两个P都可能拿到对象,这肯定不符合期望。sync.Pool在不引入互斥锁的情况下,利用了atomic包中的CAS操作。两个P都有可能拿到对象,但是当headTail最终设置的时候,只有一个P会调用CAS成功,另一个CAS会失败。02FFmpeg中的并发控制2.1问题描述最近业务需求。在探索性项目中,需要对视频进行拼接和合成。由于原始视频片段有不同的格式、大小、码率等,为了更流畅地达到最佳拼接效果,需要先对齐视频大小、编码格式、码率。在对FFmepg命令进行了一些研究,并进行了一系列的转换实验(如视频裁剪、视频填充、视频缩放等)后,都得到了预期的结果,但是当命令融入到实际业务场景中时,内存炸死进程,CPU满载任务执行时间过长甚至失败,进一步升级CPU配置的问题也没有得到太大的改善,于是开始研究FFmpeg命令的线程控制。2.2FFmpeg线程控制作为一个强大的多媒体处理工具,FFmpeg包含了多个强大的lib库。FFmpeg处理多媒体文件的过程如下:关键的计算步骤是编码、解码和数据修改。同时FFmpeg的线程控制也提供了线程控制的三个参数。在FFmpeg文档中,与线程控制相关的参数解释如下:-filter\_threadsnb\_threads(global)定义了使用多少个线程来处理一个过滤器管道。每个管道将生成一个线程池,其中包含可用于并行处理的多个线程。默认是实现对于简单过滤器的线程控制,默认的线程数是可用的CPU核心数-filter\_complex\_threadsnb\_threads(global)定义有多少线程用于处理一个filter\_complex图。类似于filter\_threads但仅用于-filter\_complex图形。默认为可用CPU数。filter\_complex\_threads实现复杂过滤器的线程控制,默认线程数也是可用CPU核数threadsinteger(decoding/encoding,video)设置线程数为被使用,以防所选的编解码器实现支持多线程。可能的值:'auto,0'自动选择要设置的线程数默认值为'auto'。threads实现编解码器的线程控制,前提是使用的编解码器支持多线程并行,文档中默认线程数文中有自动描述,但是我找遍全网都没有找到这个参数的具体描述,于是结合业务场景进行了相关数据实验使用time命令查看FFmpeg命令的耗时和CPU利用率相关参数。在4核机器上测试条件如下:-i-filter_complex-threads1-y4.54suser0.17ssystem110%cpu4.278total-i-filter_complex-threads2-y4.61suser0.29ssystem189%cpu2.581total-i-filter_complex-threads4-y4.92suser0.22ssystem257%cpu1.993total-i-filter_complex-threads6-y4.73suser0.21ssystem302%cpu1.634total-i-filter_complex-threads8-y4.72s用户0.19s系统315%cpu1.552total-i-filter_complex-y4.72s用户0.22s系统306%cpu1.614total-i-filter_complex-y-filter_complex1thread-y4.63s用户0.13s系统316%cpu1.504total-i-filter_complex-y-filter_complex_threads2-y4.62suser0.20ssystem304%cpu1.583total-i-filter_complex-y-filter_complex_threads4-y4.58suserssystem303%cpu1.599total通过实验,是发现没有线程控制,我的cropping+zooming+gblursizeflattening操作几乎没有并行空间,filter线程数增加_complex\_threads只会增加系统状态的耗时,对整体耗时和CPU利用率的增益不大。对于编解码部分,随着线程数的增加,CPU利用率增加,耗时减少,但整体数据不呈现线性关系。对于单个命令,当线程数设置为2时,基本上是一个CPU消耗和时间消耗都比较高的配置。2.3小结1.FFmepg作为计算密集型的处理工具,对CPU的需求比较大,FFmpeg提供了三个并行控制参数来控制不同类型命令的并发,但是具体命令能否并发与自身有关实现原理,需要具体问题具体分析;2、编码和解码作为FFmpeg视频处理中的关键环节,是比较耗时耗CPU的。使用好threads参数可以加快处理速度,控制CPU使用率。03paddle的静态图和动态图的Staticgraph动态图的概念静态图:类比c++,先编译再运行。因此,它可以分为两个阶段:编译期和运行期。在编译时,需要预先定义一个完整的模型,paddle会生成一个programDesc,然后使用transplier对programDesc进行优化。在运行时,执行器使用programDesc来运行。动态图:类比python,没有编译阶段,所以不需要预先定义模型。每写一行网络代码,就可以同时得到相应的计算结果。优缺点对比:静态图片:Paddle一开始只支持静态图片,所以相关的支持和文档很多。在性能方面,它也优于动态图形。但是调试起来会比较麻烦。动态图:方便调试,可以动态调整模型结构。但是执行效率很低。问题一:如何判断是静态图模式还是动态图模式静态图模式:程序中有静态模块,或者需要构建执行器,使用executor.run(program)来执行定义的模型。动态图模式:程序中有动态图模块。从paddle2.0开始,默认开启动态图模式。注意:部分API仅支持静态图/动态图。对于涉及变量值的API,一般只支持动态图。当出现imperative/dygraph等错误时,需要确认是否在静态图模式下调用了动态图api。importnumpyasnpimportpaddleimportpaddle.fluidasfluidfrompaddle.fluid.dygraph.baseimportto_variableprint(paddle.__version__)#2.1.1#静态图模式main_program=fluid.Program()startup_program=fluid.Program()paddle.enable_static()withfluid.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)#静态graph在模式下,构建占位符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('静态模式下,调用layers.data后,x=',x)#此时无法打印运行值out,output在静态模式下,调用layers.data后,x=varelementwise_add_0.tmp_0:LOD_TENSOR.shape(-1,2).dtype(float32).stop_gradient(False)place=fluid.CPUPlace()exe=fluid.执行器(放置=放置)exe.run(fluid.default_startup_program())data_after_run=exe.run(fetch_list=[x],feed={'x':data_x,'y':data_y})print('静态模式下,运行后数据:',data_after_run)#静态模式下,运行后数据:[array([[2.,2.],[2.,2.]],dtype=float32)]#使用fluid.dygraph.guard()的动态图形模式:x=np.ones([2,2],np.float32)y=np.ones([2,2],np.float32)#在动态图模式下,将numpyndarray类型数据转换为Variable类型x=fluid.dygraph.to_variable(x)y=fluid.dygraph.to_variable(y)print('InDyGraphmode,aftercallingdygraph.to_variable,x=',x)#在DyGraph模式下,在调用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('在DyGraph模式下,运行后的数据:',x.numpy())#在DyGraph模式下,运行后的数据:[[2.2.][2。2.]]问题2:如何在静态图模式下调试一般使用fluid.layers.Print()创建打印算子,打印正在访问的张量内容问题3:如何将动态图转为静态图graph基于动态图的优缺点,可以在模型开发阶段使用动态图模式,在训练和推理阶段使用静态图模式。使用@paddle.jit.to\_static修饰需要转静态和动态的函数。或者使用paddle.jit.to\_static()函数将网络作为一个整体进行转换。importnumpyasnpimportpaddleimportpaddle.fluidasfluidfrompaddle.jit导入到_staticclassMyNet(paddle.nn.Layer):def__init__(self):super(MyNet,self).__init__()self.fc=fluid.dygraph.Linear(input_dim=4,output_dim=2,act="relu")@to_staticdefforward(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)返回lossnet=MyNet()x=np.ones([16,4],np.float32)y=np.ones([16,1],np.int64)net.eval()out=net(x,y)推荐阅读:百度程序员避坑指南(手端)百度程序员避坑指南(前端)百度工程师教你的秘诀快速提升研发效率百度一线工程师谈千变万化的云原生【技术加油站】揭秘百度智能测试的规模【技术加油站】谈百度智能测试的三个阶段
