一、对象拷贝的需求与挑战
在 SystemVerilog 中,对象拷贝是一项常见且重要的操作。在实际的硬件验证和设计中,经常需要创建对象的副本以进行不同的操作,同时又不影响原始对象。这就凸显了对象拷贝的必要性。
然而,使用默认的方式进行对象拷贝可能会面临一些问题。例如,不能通过简单的 “=” 操作来实现对象的拷贝,因为这一操作实现的只是句柄的赋值而不是对象的拷贝。如果一个类中使用了另一个类的句柄,并且实例化了这个类的对象,使用 new 函数复制顶层的类的对象时,第二层的对象的句柄会被复制进去。
具体来说,浅复制会将所有的变量包括整数、字符串、实例句柄(指针)等等被复制过去。但是,浅复制是将原始对象中的所有属性拷贝到新对象中去,将句柄属性复制到新对象中去,不把 “句柄指向的对象” 复制进去,所以原始对象和新对象引用同一对象,新对象中的句柄指向的内容发生变化会导致原始对象中的对应内容也发生变化。
例如,假设有两个对象 tr1 和 tr2,如果 tr1 被复制给 tr2,此时 tr1 中的句柄 pkt 也被复制给了 tr2,而句柄本身可以理解为指针,那么相当于 tr2 和 tr1 使用了相同的指针句柄。此时如果对 tr1 或者 tr2 中的句柄指向的内容进行了修改,相当于对于 tr1 和 tr2 中 pkt 句柄共同指向的空间进行了修改。所以此时虽然仅仅修改了 tr2,但是 tr1 中 pkt 指向的空间的内容也被其同时修改了。
为了实现复制后的两个对象互相不影响,就不能使用 class 默认内建复制操作(浅复制),此时就需要采用深复制。深复制就是创建一个新的和原始句柄指向的内容相同的字段,是两个一样大的数据段,所以两者的句柄指向的空间是不同的,但内容是相通的,之后的新对象中句柄指向的内容发生改变,不会引起原始对象中句柄指向内容也发生改变。
二、浅拷贝(Shallow Copy)
(一)定义与操作
浅拷贝在 SystemVerilog 中是一种常见的对象复制方式。它先创建一个新的对象,从另一对象复制其各个类属性。所有变量都被复制,如整数、字符串、实例句柄等。然而,类属性中的对象本身并没有被复制,只有它们的句柄。例如,如果一个类包含指向另一个类的句柄,只有最高级的对象被new操作符复制,下一层的对象都不会被复制。浅复制的执行方式如下:首先为被复制的类类型对象进行分配内存空间,这个分配不会调用对象的构造函数或执行任何变量声明的初始化赋值;然后将所有的类属性,包括用于随机化和覆盖率的内部状态,复制到新对象中,对象句柄也会被复制,对于嵌入的covergroup,会将其句柄在新对象中设置为null;最后将指向新创建的对象的句柄赋值给左侧的变量。
(二)存在的问题
浅拷贝存在一些问题,主要是因为新对象与原始对象可能会引用同一对象。这意味着如果修改新对象中句柄指向的内容,原始对象中相应的内容也会发生变化。例如,有两个类baseA和B,B类中包含baseA类的实例句柄。当创建B类的两个对象b1和b2,并将b1浅拷贝给b2后,如果修改b2中baseA实例的属性,b1中对应的属性也会被修改。这种情况在实际应用中可能会导致意想不到的结果,尤其是在复杂的系统中,可能会引发错误或者难以调试的问题。浅拷贝的这种特性使得在一些情况下需要谨慎使用,或者在必要时采用深复制来避免这种问题。
三、深拷贝(Deep Copy)
(一)实现方式
为了避免浅拷贝带来的问题,需要对原有的类进行适当的修改完善,以实现对象的深拷贝。深拷贝相较浅拷贝主要在引用方面不同,深拷贝会创建一个新的和原始句柄指向的内容相同的字段,相当于有两个一样大的数据段,所以两者的句柄指向的空间是不同的,但内容是相通的。
具体实现时,需要为所有的相关类定义好copy函数。例如,假设有一个类Transaction,其中包含另一个类Packet的句柄,如果要实现Transaction类的深拷贝,不仅要复制Transaction类自身的属性,还要调用Packet类的copy函数来复制其包含的Packet对象。这样,当进行深拷贝操作时,就会将原始对象中的所有内容完整地复制到新对象中,确保新对象和原始对象完全独立。
(二)优势与应用
深拷贝的优势在于新对象中句柄指向的内容发生改变时,不会引起原始对象中句柄指向内容的改变。这使得在实际应用中,可以更加安全地对复制后的对象进行操作,而不用担心影响到原始对象。
在 UVM(通用验证方法学)中,已经集成了copy和clone操作,这为用户带来了极大的便利。用户在使用时完全不需要单独额外编写copy函数,只需调用即可。例如,在进行硬件验证时,如果需要对一个事务对象进行复制,并且不希望复制后的对象与原始对象相互影响,就可以使用 UVM 提供的copy或clone方法进行深拷贝操作。这样可以确保在不同的验证场景中,对复制后的对象进行修改时,不会意外地改变原始对象的状态,提高了验证的准确性和可靠性。同时,也减少了开发人员的工作量,提高了开发效率。
四、拷贝函数的创建与注意事项
(一)创建 copy 函数
在 SystemVerilog 中,创建 copy 函数是实现对象拷贝的关键步骤。以一个简单的类Transaction为例,创建 copy 函数首先要创建一个新的变量,如copy = new();,这会为新对象分配内存空间。然后,需要将现有变量复制到新对象中,例如copy.id = id;。如果类中包含其他类的句柄,那么也需要对其进行相应的处理。比如,如果有一个类Transaction包含另一个类Status的句柄,那么在 copy 函数中,需要像这样处理:copy.status = status.copy();,确保对包含的类也进行了正确的拷贝。这样,通过创建 copy 函数,可以实现对对象的完整拷贝,使得新对象与原始对象在内容上完全独立。
(二)类型转换与虚方法
在对象拷贝过程中,类型转换是一个需要注意的问题。SystemVerilog 中有静态转换和动态转换两种类型转换方式。静态转换在转换的表达式前加上单引号即可,但这种方式不会对转换值做检查,转换失败也无从得知。动态转换则需要使用系统函数$cast(tgt,src)做转换。当涉及到类句柄的转换时,从父类句柄转换为子类句柄需要使用$cast()函数进行转换,否则会出现编译错误。这是编译器的保护措施,防止用户出现错误的赋值。
虚方法在实现动态绑定中起着重要作用。类的多态性使得在设计和实现类时,不用担心句柄指向的对象类型是父类还是子类。通过虚方法,可以实现动态绑定,即在运行时确定句柄指向对象的类型,再动态指向应该调用的方法。例如,有一个父类basic_test和一个子类test_wr,如果将basic_test::test定义为虚方法,那么当父类句柄指向子类对象时,系统在执行方法调用时会检查句柄所指向对象的类型,进而调用正确的方法,即子类的方法。这样可以提高代码的灵活性和可维护性,在对象拷贝过程中,也可以根据实际情况利用虚方法来确保正确的拷贝行为。