Some More Ideas on Coverage Extendability

In the previous post, we looked at how to use policy classes as parameters for a highly configurable coverage collector. This allows us to easily implement different variations of what bins to ignore. If you haven't read that one yet, I'd encourage you to do so before continuing with this post.

Truthfully, using policy classes wasn't the first approach I tried. I had to go down a wrong path and fail before coming to that idea. Let's have a look at how I started out. You may remember the following code snippet showing the use of a function to specify what values are supposed to belong to a certain bin:

covergroup some_covergroup;
coverpoint some_coverpoint {
ignore_bins ignore[] = some_coverpoint with (is_ignore_bin(item));
}
endgroup

Since I had defined the covergroup for the CPU instructions inside a coverage collector class, it made the most sense to have the ignore bin function definition also part of that class. At the same time, class member function are overridable... What if we defined the ignore expression inside a virtual function? Inside the base class we don't want to ignore anything yet, so the function would be empty. We could then extend this class and override this function to ignore MUL and DIV. The code for the generic coverage collector would look like this:

class cov_collector;
covergroup cg;
coverpoint operation {
ignore_bins ignore[] = operation with (is_operation_ignore_bin(item));
}
endgroup

virtual function bit is_operation_ignore_bin(operation_e operation);
return 0;
endfunction

// ...
endclass

Unfortunately, this wasn't supported by the simulator. After a bit more fiddling I gave up on trying to use functions with the with syntax, since that didn't seem to be supported. I wasn't about to give up yet. I figured I could pass the list of ignore bins as a constructor parameter to the covergroup. Instead of having a function that tells us whether a certain value should be ignored we could write a function that returns a list of values that should be ignored:

class cov_collector;
covergroup cg(array_of_operation_e operation_ignore_bins);
coverpoint operation {
ignore_bins ignore = operation with (item inside operation_ignore_bins);
}
endgroup

virtual function array_of_operation_e get_operation_ignore_bins();
return '{};
endfunction

function new();
cg = new(get_operation_ignore_bins());
endfunction
endclass

That also gave some cryptic error. If it didn't like passing the list of ignore bins as a constructor argument, maybe it would go for having it as a class member. After some more trial and error, I eventually got it to work by declaring it as a static field:

class cov_collector;
static array_of_operation_e operation_ignore_bins;

covergroup cg;
coverpoint operation {
ignore_bins ignore = operation with (item inside operation_ignore_bins);
}
endgroup

function new();
operation_ignore_bins = get_operation_ignore_bins();
cg = new();
endfunction

// ...
endclass

For some whatever reason this worked, but only if the ignore list was static. Ignoring the fact that it's a hack, now we're back in business! We can declare our coverage collector sub-class that ignores MULs and DIVs:

class no_mul_cov_collector extends cov_collector;
virtual function array_of_operation_e get_operation_ignore_bins();
return '{ MUL, DIV };
endfunction
endclass

After firing up the simulator GUI and having a look at the generated bins, we'll get a big surprise. We'll see that MUL and DIV are, in fact, not being ignored. What's the reason for this? Well, remember that we defined the get_operation_ignore_bins() function as virtual and that we are calling this function from inside the constructor. It turns out that calling virtual functions from constructors is generally frowned upon inside the programming community, for reasons that I'm not going to list here. What's happening in our case is that the covergroup is being constructed inside the base class's constructor. The version of the bin generation function that's getting called is the one from inside that same class. Conceptually, at that point, the object being constructed is still of type cov_collector, not no_mul_cov_collector. The same behavior is also specified in the C++ standard.

Bonus points go to whoever noticed from the get-go that this whole thing was a bad idea!

In any case, we should ignore calling a class's virtual methods from its constructor altogether. This is because I've seen other simulators that will behave differently from what I've described above. I seem to remember reading that SystemVerilog should work like C++ in this respect, but I couldn't find any reference inside the LRM. If anyone could point out the appropriate section in the comments, that would be great!

I guess we'll have to scratch that idea... Not necessarily. We can still make use of polymorphism, but we just need to take the method that generates the bins out of the coverage collector class and put it into some other class. We'll call this a policy class:

class cg_ignore_bins_policy;
virtual function array_of_operation_e get_operation_ignore_bins();
return '{};
endfunction
endclass

The coverage collector class would get an instance of this policy class:

class cov_collector;
protected string name;
static operation_e operation_ignore_bins[];

function new(string name, cg_ignore_bins_policy policy);
this.name = name;
operation_ignore_bins = policy.get_operation_ignore_bins();
endfunction

// ...
endclass

We can now subclass the ignore bin generation policy as we like and pass different types of objects to the coverage collector. The simulator will take care at run time that the appropriate get_operation_ignore_bins() function gets called. For example, to create a coverage collector that ignores MULs and DIVs we can create a policy that specifies those as ignore bins:

class no_mul_cg_ignore_bins_policy extends cg_ignore_bins_policy;
virtual function array_of_operation_e get_operation_ignore_bins();
return '{ MUL, DIV };
endfunction
endclass

When instantiating the coverage collector, we pass it an object of the desired policy subclass:

no_mul_cg_ignore_bins_policy no_mul_policy = new();
cov_collector no_mul_cov = new("no_mul_cov", no_mul_policy);

We can generalize this approach for the other coverpoints as well. Here's how the full covergroup would look like:

class cov_collector;
protected string name;

static operation_e operation_ignore_bins[];
static register_e op1_ignore_bins[];
static register_e op2_ignore_bins[];
static register_e dest_ignore_bins[];


covergroup cg() with function sample(operation_e operation, register_e op1,
register_e op2, register_e dest);

option.per_instance = 1;
option.name = name;

coverpoint operation {
ignore_bins ignore[] = operation with (item inside
{ operation_ignore_bins });
}

coverpoint op1 {
ignore_bins ignore[] = op1 with (item inside { op1_ignore_bins });
}

coverpoint op2 {
ignore_bins ignore[] = op2 with (item inside { op2_ignore_bins });
}

coverpoint dest {
ignore_bins ignore[] = dest with (item inside { dest_ignore_bins });
}

operation_vs_op1 : cross operation, op1;
operation_vs_op2 : cross operation, op2;
operation_vs_dest : cross operation, dest;

same_reg_both_ops : coverpoint (op1 == op2);
same_reg_op1_and_dest : coverpoint (op1 == dest);
same_reg_op2_and_dest : coverpoint (op2 == dest);
same_reg_both_ops_and_dest : coverpoint (op1 == dest && op2 == dest);
endgroup


function new(string name, cg_ignore_bins_policy policy);
this.name = name;
operation_ignore_bins = policy.get_operation_ignore_bins();
op1_ignore_bins = policy.get_op1_ignore_bins();
op2_ignore_bins = policy.get_op2_ignore_bins();
dest_ignore_bins = policy.get_dest_ignore_bins();
cg = new();
endfunction


function void sample(instruction instr);
cg.sample(instr.operation, instr.op1, instr.op2, instr.dest);
endfunction
endclass

Using a different policy we can implement the case where we only have four registers:

class less_regs_cg_ignore_bins_policy extends cg_ignore_bins_policy;
virtual function array_of_register_e get_op1_ignore_bins();
return '{ R4, R5, R6, R7 };
endfunction

virtual function array_of_register_e get_op2_ignore_bins();
return get_op1_ignore_bins();
endfunction

virtual function array_of_register_e get_dest_ignore_bins();
return get_op1_ignore_bins();
endfunction
endclass

It's also very easy to implement the case with less registers and without multiplication:

class less_regs_no_mul_cg_ignore_bins_policy extends
less_regs_cg_ignore_bins_policy;

virtual function array_of_operation_e get_operation_ignore_bins();
no_mul_cg_ignore_bins_policy no_mul_policy = new();
return no_mul_policy.get_operation_ignore_bins();
endfunction
endclass

As we saw above, parameterizing the coverage definitions is done by feeding the coverage collector the appropriate policy:

cg_ignore_bins_policy policy = new();
cov_collector cov = new("cov", policy);

no_mul_cg_ignore_bins_policy no_mul_policy = new();
cov_collector no_mul_cov = new("no_mul_cov", no_mul_policy);

less_regs_cg_ignore_bins_policy less_regs_policy = new();
cov_collector less_regs_cov = new("less_regs_cov", less_regs_policy);

less_regs_no_mul_cg_ignore_bins_policy less_regs_no_mul_policy = new();
cov_collector less_regs_no_mul_cov = new("less_regs_no_mul_cov",
less_regs_no_mul_policy);

You may notice that this approach is remarkably similar to the one we used in the previous post. Before, the type of policy that the coverage collector used was specified at compile time, by passing the policy class as a parameter and relying on static methods. Now, the coverage collector has to wait until run time to get the policy via a constructor argument and it makes use of virtual methods and polymorphism. We could describe the former approach as static and the latter as dynamic.

The dynamic approach seems to suffer from worse tool support, though. We've only got it running by employing various hacks. Barring that we could say that the two are, for all intents and purposes, equivalent.

Or are they? Make sure to read the next post where we'll explore how the two styles fare in the context of a UVM environment.

Comments