十二 · 2023年08月08日

给仿真加点速

✎ 编 者 按 

昨晚看SpinalHDL的Issues,其中有一个关于性能提升的case:https://github.com/SpinalHDL/SpinalHDL/issues/1165 吸引到了我,尝试实验到深夜,测试下在SpinalHDL以及cocotb下的性能优化手段。

SpinalHDL Simulation性能提升测试

无论是SpinalHDL还是cocotb,其在仿真方面所采用的思路是一样的。在SpinalHDL的那个Issue里,Dolu主要想做的是尽可能避免在信号的赋值和读取上的冗余代码。以toInt为例,其会调用getInt函数:

private def getInt(bt: BaseType): Int = {    if(bt.getBitsWidth == 0) return 0    val manager = SimManagerContext.current.manager    val signal = btToSignal(manager, bt)    manager.getInt(signal)  }

而Dolu的思路则是没有必要每次都重新寻找manager、signal这些信息,毕竟对于一个信号而言这两个值是不变的。而是提前准备好对于这种频繁使用的接口则能够尽可能降低不必要的开销。

这里做了个测试,testSimNormal采用普通的API调用形式访问信号,

testSimSpeed则采用加速后的方式进行访问一个512bit信号位宽。两者测试访问1亿次信号值所消耗的时间,测试结果如下:

image.png

可以看出,还是能够加速仿真速度的。

考虑到在仿真过程中无非是信号的驱动和读取,那么这里应该是都适用的,遂以相同的DUT相同的Case尝试做了如下测试:

  • testSim1:采用SpinalHDL原生API进行仿真测试
  • testSim2:将信号的读取和赋值均改为优化后的方式
  • testSim3:在testSim2的基础上将时钟,复位驱动也改为优化后的方式
  • testSim4:在testSim2的基础上将waitSampling修改为优化后的方式
  • testSim5:在testSim3的基础上将时钟,将waitSampling修改为优化后的方式

测试结果如下:

image.png

每个测试里面都是跑了500000周期,可以看到,对于降低延迟还是有效果的。对于更大的case,也许会有更有效的效果。

附上完整的测试代码(由于电脑较差,诸君可自行测试):

import spinal.core._import spinal.lib._import spinal.sim.{Signal, SimManager, SimManagerContext}import scala.collection.mutable.ArrayBuffercase class dut() extends Component {  val io = new Bundle {    val data_in = slave Flow (UInt(512 bits))    val data_out = master Flow (UInt(512 bits))  }  noIoPrefix()  io.data_out << io.data_in.translateWith(io.data_in.payload + 1).stage()}import spinal.core.sim._object testSimNormal extends App {  SimConfig.withFstWave.compile(dut()).doSim { dut =>    dut.io.data_in.valid #= false    dut.clockDomain.forkStimulus(10)    dut.clockDomain.waitSampling(10)    val startTime = System.currentTimeMillis()    for (index <- 0 until 100000000) {      dut.io.data_out.payload.toBigInt    }    val endTime = System.currentTimeMillis()    val totalTime = endTime - startTime    println("代码运行时间:" + totalTime + "毫秒")  }}object testSimSpeed extends App {  implicit class SimBitVectorPimper(bt: BaseType) {    class SimProxy(bt: BaseType) {      val manager = SimManagerContext.current.manager      val signal = manager.raw.userData.asInstanceOf[ArrayBuffer[Signal]](bt.algoInt)      val alwaysZero = bt.getBitsWidth == 0      def getLong = manager.getLong(signal)      def getBoolean = manager.getLong(signal) != 0      def getBigInt = manager.getBigInt(signal)      def assignBoolean(value: Boolean) = manager.setLong(signal, value.toInt)      def setLong(value: Long) = manager.setLong(signal, value)      def assignBigInt(value: BigInt) = manager.setBigInt(signal, value)    }    def simProxy() = new SimProxy(bt)  }  SimConfig.withFstWave.compile(dut()).doSim { dut =>    dut.io.data_in.valid #= false    dut.clockDomain.forkStimulus(10)    dut.clockDomain.waitSampling(10)    val dataOutHdl = dut.io.data_out.payload.simProxy()    val startTime = System.currentTimeMillis()    for (index <- 0 until 100000000) {      dataOutHdl.getBigInt    }    val endTime = System.currentTimeMillis()    val totalTime = endTime - startTime    println("代码运行时间:" + totalTime + "毫秒")  }}object SimExtend {  implicit class SimBitVectorPimper(bt: BaseType) {    class SimProxy(bt: BaseType) {      val manager = SimManagerContext.current.manager      val signal = manager.raw.userData.asInstanceOf[ArrayBuffer[Signal]](bt.algoInt)      val alwaysZero = bt.getBitsWidth == 0      def getLong = manager.getLong(signal)      def getBoolean = manager.getLong(signal) != 0      def getBigInt = manager.getBigInt(signal)      def assignBoolean(value: Boolean) = manager.setLong(signal, value.toInt)      def setLong(value: Long) = manager.setLong(signal, value)      def assignBigInt(value: BigInt) = manager.setBigInt(signal, value)    }    def simProxy() = new SimProxy(bt)  }  def getBool(manager: SimManager, who: Bool): Bool = {    val component = who.component    if ((who.isInput || who.isOutput) && component != null && component.parent == null) {      who    } else {      manager.userData.asInstanceOf[Component].pulledDataCache.getOrElse(who, null).asInstanceOf[Bool]    }  }}object testSim extends App {  val dutCompiled = SimConfig.withFstWave.compile(dut())  /** *****************************************************************************************   * testSim1   * ***************************************************************************************** */  dutCompiled.doSim { dut =>    dut.io.data_in.valid #= false    dut.clockDomain.forkStimulus(10)    dut.clockDomain.waitSampling(10)    var sum = BigInt(0)    for (index <- 0 until 500000) {      dut.clockDomain.waitSampling()      if (dut.io.data_out.valid.toBoolean) {        sum = sum + dut.io.data_out.payload.toBigInt      }      dut.io.data_in.valid #= true      dut.io.data_in.payload #= BigInt(index)    }  }  /** *****************************************************************************************   * testSim2   * ***************************************************************************************** */  dutCompiled.doSim { dut =>    import SimExtend._    val dataInValidHdl = dut.io.data_in.valid.simProxy()    val dataInDataHdl = dut.io.data_in.payload.simProxy()    val dataOutValidHdl = dut.io.data_out.valid.simProxy()    val dataOutDataHdl = dut.io.data_out.payload.simProxy()    dataInValidHdl.assignBoolean(false)    dut.clockDomain.forkStimulus(10)    dut.clockDomain.waitSampling(10)    var sum = BigInt(0)    for (index <- 0 until 500000) {      dut.clockDomain.waitSampling()      if (dataOutValidHdl.getBoolean) {        sum = sum + dataOutDataHdl.getBigInt      }      dataInValidHdl.assignBoolean(true)      dataInDataHdl.assignBigInt(index)    }  }  /** *****************************************************************************************   * testSim3   * ***************************************************************************************** */  dutCompiled.doSim { dut =>    import SimExtend._    val dataInValidHdl = dut.io.data_in.valid.simProxy()    val dataInDataHdl = dut.io.data_in.payload.simProxy()    val dataOutValidHdl = dut.io.data_out.valid.simProxy()    val dataOutDataHdl = dut.io.data_out.payload.simProxy()    val clock = getBool(SimManagerContext.current.manager, dut.clockDomain.clock).simProxy()    val reset = getBool(SimManagerContext.current.manager, dut.clockDomain.reset).simProxy()    dataInValidHdl.assignBoolean(false)    //clock generation    clock.assignBoolean(false)    reset.assignBoolean(true)    sleep(10 * 16)    reset.assignBoolean(false)    fork {      var value = false      def t: Unit = {        value = !value        clock.assignBoolean(value)        delayed(5)(t)      }      t    }    dut.clockDomain.waitSampling(10)    var sum = BigInt(0)    for (index <- 0 until 500000) {      dut.clockDomain.waitSampling()      if (dataOutValidHdl.getBoolean) {        sum = sum + dataOutDataHdl.getBigInt      }      dataInValidHdl.assignBoolean(true)      dataInDataHdl.assignBigInt(index)    }  }  /** *****************************************************************************************   * testSim4   * ***************************************************************************************** */  dutCompiled.doSim { dut =>    import SimExtend._    val dataInValidHdl = dut.io.data_in.valid.simProxy()    val dataInDataHdl = dut.io.data_in.payload.simProxy()    val dataOutValidHdl = dut.io.data_out.valid.simProxy()    val dataOutDataHdl = dut.io.data_out.payload.simProxy()    val clock = getBool(SimManagerContext.current.manager, dut.clockDomain.clock).simProxy()    val reset = getBool(SimManagerContext.current.manager, dut.clockDomain.reset).simProxy()    var rising = false    var last = false    dataInValidHdl.assignBoolean(false)    //clock generation    dut.clockDomain.forkStimulus(10)    dut.clockDomain.waitSampling(10)    var sum = BigInt(0)    for (index <- 0 until 500000) {      waitUntil {        rising = false        val current = clock.getBoolean        if ((!last) && current) {          rising = true        }        last = current        rising      }      if (dataOutValidHdl.getBoolean) {        sum = sum + dataOutDataHdl.getBigInt      }      dataInValidHdl.assignBoolean(true)      dataInDataHdl.assignBigInt(index)    }  }  /** *****************************************************************************************   * testSim5   * ***************************************************************************************** */  dutCompiled.doSim { dut =>    import SimExtend._    val dataInValidHdl = dut.io.data_in.valid.simProxy()    val dataInDataHdl = dut.io.data_in.payload.simProxy()    val dataOutValidHdl = dut.io.data_out.valid.simProxy()    val dataOutDataHdl = dut.io.data_out.payload.simProxy()    val clock = getBool(SimManagerContext.current.manager, dut.clockDomain.clock).simProxy()    val reset = getBool(SimManagerContext.current.manager, dut.clockDomain.reset).simProxy()    var rising = false    var last = false    dataInValidHdl.assignBoolean(false)    //clock generation    clock.assignBoolean(false)    reset.assignBoolean(true)    sleep(10 * 16)    reset.assignBoolean(false)    fork {      var value = false      def t: Unit = {        value = !value        clock.assignBoolean(value)        delayed(5)(t)      }      t    }    dut.clockDomain.waitSampling(10)    var sum = BigInt(0)    for (index <- 0 until 500000) {      waitUntil {        rising = false        val current = clock.getBoolean        if ((!last) && current) {          rising = true        }        last = current        rising      }      if (dataOutValidHdl.getBoolean) {        sum = sum + dataOutDataHdl.getBigInt      }      dataInValidHdl.assignBoolean(true)      dataInDataHdl.assignBigInt(index)    }  }}

cocotb性能优化

cocotb的仿真速度一直我是持保留意见的。 在SpinalHDL里面做完尝试,最近工作里用到的cocotb较多,就尝试看下能否应用到cocotb中。看了下cocotb中的信号读写封装背后的调用,其做了太多的封装和调用。遂采用了相同的DUT做了同样的测试。首先是做优化前后的一百万次的方式测试(跑一亿次真的太久了)

image.png

可以看到,这里有明显的性能提升。

再来构建下面的六个case:

  • testCase0 :采用cocotb提供的API接口进行数据读写访问
  • testCase1: 仅将信号读更改为底层接口直接调用形式进行访问
  • testCase2:将信号读,信号写均改为底层接口直接调用形式进行访问
  • testCase3:在testCase2的基础上将信号接口提前生成好而不是使用时例化
  • testCase4:在testCase4的基础上将时钟生成修改为底层接口直接调用形式
  • testCase5: 在testCase0基础上,仅将时钟生成修改为底层接口直接调用的形式

测试结果如下:

image.png

每个Case中均做100000次周期测试。可以看到,与原生Case仿真相比,testCase5能提升1.4倍多,而testCase4则有2.8倍的性能提升(贴图有误)。由此可见,cocotb中对于信号读写的封装由于做了太多安全和边界的处理导致这种在仿真中经常使用的函数带来挺大的开销。

由于Verilator好像不支持时钟下沉,如果将时钟的驱动给放到Verilog里面,也许还会有进一步的性能提升。

本人对于底层的东西不甚了解,单纯从仿真速度上,cocotb相较于SpinalHDL还是有较大的差距(《既生瑜何生亮——SpinalHDL VS Cocotb》),有一点有意思的额是在SpinalHDL里面修改时钟生成的方式并未有太大的性能提升,而在cocotb里确有明显改善,诸君有兴趣可以自行研究。

附上源码,感兴趣的小伙伴可以自行测试:

DUT:

// Generator : SpinalHDL v1.8.0b git head : 761a30e521263983ddf14de3592f7a9f38bf0589// Component : simSpeedUpTest`timescale 1ns/1psmodule dut (  input data_in_valid,  output reg data_out_valid,  input [511:0] data_in,  output reg [511:0] data_out,  input clk,  input reset); always @(posedge clk ) begin    if(reset) begin      data_out <= 'd0;      data_out_valid<='d0;    end else begin      data_out <= data_in+1;      data_out_valid<= data_in_valid;    end  endendmodule

TestBench:

import cocotbfrom cocotb_bus.drivers import BusDriverfrom cocotb.clock import Clockfrom cocotb.triggers import ClockCycles,RisingEdge,Timer,ReadOnlyfrom cocotb.handle import *@cocotb.test(skip=False)async def testCaseNormal(dut):    targetDataSignal=dut.data_out._handle    targetValueSignal=dut.data_out_valid._handle    dataInvalidSignal=dut.data_in_valid._handle    dataInDataSignal=dut.data_in._handle    cocotb.start_soon(generateClk(dut.clk))    dataInDataSignal.set_signal_val_binstr(0,bin(0)[2:])    dataInvalidSignal.set_signal_val_int(0,0)    dut.reset.value=1    await ClockCycles(dut.clk,10)    dut.reset.value=0    await ClockCycles(dut.clk,10)    sum=0    for index in range(1000000):        dut.data_out_valid.value    await ClockCycles(dut.clk,10)@cocotb.test(skip=False)async def testCaseSpeed(dut):    targetDataSignal=dut.data_out._handle    targetValueSignal=dut.data_out_valid._handle    dataInvalidSignal=dut.data_in_valid._handle    dataInDataSignal=dut.data_in._handle    cocotb.start_soon(generateClk(dut.clk))    dataInDataSignal.set_signal_val_binstr(0,bin(0)[2:])    dataInvalidSignal.set_signal_val_int(0,0)    dut.reset.value=1    await ClockCycles(dut.clk,10)    dut.reset.value=0    await ClockCycles(dut.clk,10)    sum=0    for index in range(1000000):        targetDataSignal.get_signal_val_binstr()    await ClockCycles(dut.clk,10)@cocotb.test(skip=False)async def testCase0(dut):    cocotb.start_soon(Clock(dut.clk,10,'ns').start())    dut.reset.value=1    dut.data_in.value = 0    dut.data_in_valid.value = 0    await ClockCycles(dut.clk,10)    dut.reset.value=0    await ClockCycles(dut.clk,10)    sum=0    targetSignal=dut.data_out._handle    for index in range(100000):        await RisingEdge(dut.clk)        if int(dut.data_out_valid.value) == 1:            sum+= dut.data_out.value        dut.data_in_valid.value = 1        dut.data_in.value = index    await ClockCycles(dut.clk,10)@cocotb.test(skip=False)async def testCase1(dut):    cocotb.start_soon(Clock(dut.clk,10,'ns').start())    dut.data_in.value = 0    dut.data_in_valid.value = 0    dut.reset.value=1    await ClockCycles(dut.clk,10)    dut.reset.value=0    await ClockCycles(dut.clk,10)    sum=0    targetDataSignal=dut.data_out._handle    targetValueSignal=dut.data_out_valid._handle    for index in range(100000):        await RisingEdge(dut.clk)        if targetValueSignal.get_signal_val_long()==1:            sum+= int(targetDataSignal.get_signal_val_binstr(),2)        dut.data_in_valid.value = 1        dut.data_in.value = index    await ClockCycles(dut.clk,10)@cocotb.test(skip=False)async def testCase2(dut):    cocotb.start_soon(Clock(dut.clk,10,'ns').start())    dut.data_in.value = 0    dut.data_in_valid.value = 0    dut.reset.value=1    await ClockCycles(dut.clk,10)    dut.reset.value=0    await ClockCycles(dut.clk,10)    sum=0    targetDataSignal=dut.data_out._handle    targetValueSignal=dut.data_out_valid._handle    for index in range(100000):        await RisingEdge(dut.clk)        if targetValueSignal.get_signal_val_long()==1:            sum+= int(targetDataSignal.get_signal_val_binstr(),2)        dut.data_in._handle.set_signal_val_binstr(0,bin(index)[2:])        dut.data_in_valid._handle.set_signal_val_int(0,1)    await ClockCycles(dut.clk,10)@cocotb.test(skip=False)async def testCase3(dut):    cocotb.start_soon(Clock(dut.clk,10,'ns').start())    dut.data_in.value = 0    dut.data_in_valid.value = 0    dut.reset.value=1    await ClockCycles(dut.clk,10)    dut.reset.value=0    await ClockCycles(dut.clk,10)    sum=0    targetDataSignal=dut.data_out._handle    targetValueSignal=dut.data_out_valid._handle    dataInvalidSignal=dut.data_in_valid._handle    dataInDataSignal=dut.data_in._handle    for index in range(100000):        await RisingEdge(dut.clk)        if targetValueSignal.get_signal_val_long()==1:            sum+= int(targetDataSignal.get_signal_val_binstr(),2)        dataInDataSignal.set_signal_val_binstr(0,bin(index)[2:])        dataInvalidSignal.set_signal_val_int(0,1)    await ClockCycles(dut.clk,10)async def generateClk(clk):    clk._handle.set_signal_val_int(1,0)    while True:        await Timer(5, units="ns")        clk._handle.set_signal_val_int(0,0)        await Timer(5, units="ns")        clk._handle.set_signal_val_int(0,1)@cocotb.test(skip=False)async def testCase4(dut):    targetDataSignal=dut.data_out._handle    targetValueSignal=dut.data_out_valid._handle    dataInvalidSignal=dut.data_in_valid._handle    dataInDataSignal=dut.data_in._handle    cocotb.start_soon(generateClk(dut.clk))    dataInDataSignal.set_signal_val_binstr(0,bin(0)[2:])    dataInvalidSignal.set_signal_val_int(0,0)    dut.reset.value=1    await ClockCycles(dut.clk,10)    dut.reset.value=0    await ClockCycles(dut.clk,10)    sum=0    for index in range(100000):        await RisingEdge(dut.clk)        if targetValueSignal.get_signal_val_long()==1:            sum+= int(targetDataSignal.get_signal_val_binstr(),2)        dataInDataSignal.set_signal_val_binstr(0,bin(index)[2:])        dataInvalidSignal.set_signal_val_int(0,1)    await ClockCycles(dut.clk,10)@cocotb.test(skip=False)async def testCase5(dut):    cocotb.start_soon(generateClk(dut.clk))    dut.reset.value=1    dut.data_in.value = 0    dut.data_in_valid.value = 0    await ClockCycles(dut.clk,10)    dut.reset.value=0    await ClockCycles(dut.clk,10)    sum=0    targetSignal=dut.data_out._handle    for index in range(100000):        await RisingEdge(dut.clk)        if int(dut.data_out_valid.value) == 1:            sum+= dut.data_out.value        dut.data_in_valid.value = 1        dut.data_in.value = index    await ClockCycles(dut.clk,10)

END

作者:玉骐
原文链接:Spinal FPGA
微信公众号:
 title=

推荐阅读

更多SpinalHDL技术干货请关注[Spinal FPGA]欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。
推荐阅读
关注数
1578
内容数
131
用SpinalHDL提升生产力
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息