落とし穴を避けるためのBaiduプログラマー開発ガイド(3)

写真

最初の2つの問題では、日常業務におけるフロントエンドとモバイルの開発に関連する問題を共有しました。興味のある学生は、記事の最後に読んでジャンプして表示することをお勧めします。この号では、gc圧力を低減するgolangオブジェクトプール、FFmpegでの同時実行制御、パドルの静的グラフと動的グラフの3つのトピックを共有し、テクノロジーの向上に役立つことを期待しています。

01golangオブジェクトプールはgc圧力を低減します

sync.Poolは、Golangに組み込まれているオブジェクトプールテクノロジーであり、一時オブジェクトをキャッシュして、一時オブジェクトの頻繁な作成やGCへの負荷によって引き起こされる消費を回避できます。sync.Poolによってキャッシュされたオブジェクトはいつでも通知されない可能性があるため、sync.Poolを使用して永続オブジェクトを保存することはできません。sync.Poolは、同時実行性に安全であるだけでなく、アトミックパッケージにCAS操作を導入することでロックフリーを実現します。CPUおよびオペレーティングシステムレベルに近いアトミック操作を通じて、ロックを置き換える並行シナリオのニーズを満たします。

1.1使用

sync.Poolが初期化されるとき、ユーザーはオブジェクトコンストラクターNewを提供する必要があります。ユーザーは、Getを使用してオブジェクトプールからオブジェクトを取得し、Putを使用してオブジェクトをオブジェクトプールに返します。全体の使用法は比較的簡単です。

1.2原則

GMPスケジューリングモデルでは、スレッドディメンションから、Pのロジックは単一のスレッドによって実行されます。これは、Pのコルーチンの同時実行性を解決するための条件を提供します。sync.Poolは、GMPのこの機能を最大限に活用することです。同じsync.Poolの場合、各Pには独自のローカルオブジェクトプールpoolLocalがあります。各Pは、独自のローカルオブジェクトプールpoolLocalに対応します。これは、Pのローカルオブジェクトを格納するメモリプールです。各poolLocalは、プライベートとpoolChainに対応します。poolChainは、複数のringBufferを指すリンクリストです。RingBufferは、リング構造がメモリの再利用に便利であり、ringBufferはCPUキャッシュに役立つ連続メモリであるために使用されます。

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,没有编译阶段,所以不用预先定义模型。每写一行网络代码,即可同时获得对应计算结果。

优缺点对比:

静止画像:パドルは最初は静止画像のみをサポートしているため、関連するサポートやドキュメントが多数あります。パフォーマンスの面でも、ダイナミックグラフィックスよりも優れています。ただし、デバッグはさらに面倒になります。
動的ダイアグラム:デバッグに便利で、モデル構造を動的に調整できます。しかし、実行効率は低いです。

質問1:現在静止画像モードか動的画像モードかを判断する方法

  • 静的グラフモード:プログラムに静的モジュールがあるか、エグゼキューターをビルドし、executor.run(program)を使用して定義されたモデルを実行する必要があります。

  • 動的グラフモード:dygraphモジュールがプログラムに存在します。パドル2.0以降、動的グラフモードはデフォルトで有効になっています。

  • 注:一部のAPIは静的グラフ/動的グラフのみをサポートします。たとえば、変数値を含むAPIは通常、動的グラフのみをサポートします。命令型/ダイグラフなどでエラーが発生した場合は、静的グラフモードで動的グラフ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()を使用して、アクセスされているテンソルのコンテンツを印刷するための印刷演算子を作成します。

質問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)
复制代码

推奨読書:

Baiduプログラマーがピット回避ガイド(モバイル端末)を作成

ピットを回避するためのBaiduプログラマー開発ガイド(フロントエンド)

Baiduのエンジニアが、研究開発の効率をすばやく向上させるためのヒントを教えてくれます

Baiduの第一線のエンジニアが、絶えず変化するクラウドネイティブについて語ります

【テクニカルガソリンスタンド】Baiduのインテリジェントテストの大規模な実装を明らかにする

【テクニカルガソリンスタンド】Baiduの知能検査の3段階について語る

おすすめ

転載: juejin.im/post/7085620918998794276