Research

How to improve the performance of USRP/SDR (如何提升USRP/SDR性能)

在过去的一年中,我们将PicoScenes on SDR的性能从最初的惨不忍睹,逐步提升到可以支持实时解析与交互式测量。这经历了多方面的优化与改善,以下仅就USRP相关的性能提升经历做一些分享。其中一部分经验也适用于其它品牌软件无线电(SDR)设备。

背景

具体到PicoScenes平台,整个平台用C++编写,最核心的PicoScenes on SDR(Wi-Fi基带处理代码)也是用C++编写。

接收流程:我们使用USRP的驱动程序UHD直接从USRP设备读取到原始的基带信号。这些基带信号被送入PicoScenes on SDR处理模块,解析,返回MAC数据包,并交给PicoScenes平台做进一步处理。

发送流程:上层插件(PicoScenes Plugins)调用平台API生成MAC数据包,交给PicoScenes on SDR生成基带信号,通过UHD驱动把信号交给USRP设备并发射出去。

我们没有使用GNURadio,所以以下的经验都围绕USRP、UHD、高性能基带I/O展开。

官方优化技巧

在做任何性能优化之前,你应该参考USRP官方性能优化Tips

DPDK:官方建议里的DPDK这项优化,性能提升有限,但却配置很麻烦。对于PicoScenes系统而言,DPDK会在客户端系统上提出过于复杂的配置要求,这并不友好。因此,我们没有采用DPDK。

进程及线程优先级:根据我们的测试体验,提升进程及线程的优化级对性能的提升很难感知。这是因为,PicoScenes系统本身就是多线程构架的。当其运行在多核心CPU上时,Linux内核的调度器就已经可以很好的调度分配CPU资源。或者说,提升进程及线程优先级,仅在CPU核心数有限的情况下,才存在优化的意义。然而,核心数较少的CPU一般都比较老旧,其IPC及主频可能并不高,在老旧CPU上运行PicoScenes on SDR的性能会很差。

超线程技术(HT):根据我们的测试,关闭HT可以显著地提升CPU的单核心性能,这对于改善PicoScenes on SDR的性能有比较明显的帮助(是的,PicoScenes on SDR的核心目前是单线程执行,我们正在进行多线程改造)。在我们的测试中,关闭HT可以提升大约15%左右自己能。

关闭Spectre/Meltdown漏洞防护:Spectre/Meltdown漏洞的主要攻击面是预测执行机制。PicoScenes on SDR的核心在开启全面优化后,需要充分利用CPU的预测执行机制。因此,您可以在谨慎评估安全风险之后,关闭漏洞防护。这可以提升5%左右性能。

不要使用float及double类型CPU-format

TL;DR: UHD驱动中的“wire-format”与“CPU-format”转换在高采样率时会成为瓶颈,导致Rx overrun或Tx underrun。

什么是“wire-format”与“CPU-format”? USRP设备在硬件层面,能支持且仅支持两种数据类型:16-bit整型与8-bit整型,这被称为wire-format。UHD驱动为了方便上层基带程序的编写,在软件层面还额外提供了对float/double类型的支持,这就是cpu-format。UHD作为硬件与上层基带处理程序之间的媒介,提供了自动的cpu-format<->wire-format之间的转换,也就是说,它提供了int->float/double及float/double->int的自动转换。

自动转换,有什么不好?自动转换本身没有不好,并且UHD核心的自动转换编写的非常非常高效,甚至是直接在C++代码中嵌入SIMD汇编指令实现的。其最大的问题,自动转换把CPU性能瓶颈带来的数据丢失问题隐藏在用户无法优化的地方

这是什么意思?当基带采样率较高时,前述的格式自动转换就会成为I/O吞吐的瓶颈。这会造成什么?Rx overflow 或Tx underrun!这两个问题会进一步造成Rx端收到的数据缺失不完整,或者Tx端发送的数据缺失不完整。 所以,也就是说,在高基带采样率时(20MHz以上),自动类型转换本身,会成为性能瓶颈,并造成发送或接收数据的不连续性。

那么,怎么办?自己手工做类型转换,并与UHD发送/接收8-bit/16-bit整型数据

可是,这没有性能问题么?如果充分优化,没有问题,甚至比UHD的更好。以下代码,就是PicoScenes中使用的int16->double类型转换代码。

for (auto i = 0, curPos = 0; i < rxChannelList.size(); i++) {
    std::transform(std::execution::par_unseq, rxSignalBuffer[i].cbegin() + bufferTail, rxSignalBuffer[i].cbegin() + bufferTail + currentStep, tempBuffer.begin(), [](const std::complex<int16_t> dc) -> std::complex<double> {
         return std::complex<double>{static_cast<double>(dc.real()) / 32767.0, static_cast<double>(dc.imag()) / 32767.0};
    });
    curPos += currentStep;
}

以上代码,是把多个Rx Channel的数量合并在一列,并使用std::transform把rxBuffer中的std::complex<int16_t> 类型转换为std::complex<double>类型。看起来会很慢么? 其实不会,因为开启SSE4.2/AVX/AVX2指令集之后,以上代码会被深度加速,其性能并不输于UHD中内嵌的向量计算代码。

其中,另一个重要的优化,就是并行化transform,即通过指定std::execution::par_unseq这个EXECUTION_POLICY实现的。这也是C++20带来的一个新特性,其真正的执行底层是通过Intel TBB层实现的。因此,通过开启多线程并行化 + 开启向量计算指令集,以上类型转换代码性能非常高,甚至要超过UHD版本。

使用面向CPU构架的编译优化

一般情况下,release模式编译仅开启了-o3优化,但这实际上是兼容性最高的优化选项,CPU的向量计算指令集并没有使用。PicoScenes on SDR的基带处理模块包含大量的数学计算,通过开启向量计算指令集可以实现性能的飞跃。

对数学计算最重要的3个指令集分别是SSE4.2,AVX以及AVX2。那么,哪些CPU提供了这些指令集呢?Intel 1代Core及上处理器加入了SSE4.2指令集,Intel 3代Core及以上处理器加入了AVX指令集,Intel 4代Core及以上处理器加入了AVX2指令集; AMD 的CPU系列中,Excavator构架之后的所有处理器都同时支持SSE 4.2, AVX, AVX2这三种指令集。

参考GCC官方的“-m参数”一节,我们确定了在硬件兼容性及性能优化方面最平衡的参考配置:

-march=ivybridge -mtune=skylake

ivybridge是Intel 3代Core处理器的代号,skylake则对应7代Core处理器。

最有效的手段:使用高性能电脑(这不是玩笑)

目前,我们的计算机配置是Intel i9-10900K + 水冷散热,Z490主板,32GB DDR4 3200MHz内存,512GB SSD。

其中最重要的当然是CPU,10900K处理器可以在超频状态下稳定5.3GHz主频,加上AVX2指令集加持,再加上10核心(关闭超线程)加持,它可以提供目前可获取的最高CPU性能(截止2021年1月)。

之所以选择如此高主频的处理器,而不是AMD Threadripper这样的多核心处理器,主要原因是,在目前阶段我们的基带算法还是单线程执行,还无法充分利用多核心性能。

高性能SSD也很重要,当进行高采样率+多USRP并行基带信号录制/回放时,高性能SSD可以保证I/O写入不会成为瓶颈。 但在购买SSD时,请注意一点:相比传统硬盘,SSD是有“写入寿命”(最大可写入数据量)的,比如500TB Write。 如果进行长时间的高采样率+多USRP并行基带信号录制时,这个上限甚至可以在1天内被突破!