Skip to content

mem-cache: Add profiling statistics to replacement policy#3209

Open
HirunaVishwamith wants to merge 1 commit into
gem5:developfrom
HirunaVishwamith:feature-rp-profiling
Open

mem-cache: Add profiling statistics to replacement policy#3209
HirunaVishwamith wants to merge 1 commit into
gem5:developfrom
HirunaVishwamith:feature-rp-profiling

Conversation

@HirunaVishwamith
Copy link
Copy Markdown

The cache replacement policies did not expose any statistics, so their behavior could not be profiled. This change adds profiling counters that are shared by every replacement policy.

The public interface (invalidate, touch, reset and getVictim) is turned into a Non-Virtual Interface: the public methods are now small wrappers in replacement_policy::Base that gather statistics and then forward to protected *Impl methods implemented by each policy. Keeping the counters in one place means no policy can accidentally bypass them, and the callers in the cache tags are left unchanged.

The new statistics record the number of victimizations, hits, insertions and invalidations, plus a formula for the average number of hits an entry receives before being replaced. WeightedLRU keeps its occupancy-aware touch() overload, which is now routed through the common interface so the access is also counted.

All policies are updated to implement the *Impl methods. A unit test for the new counters is added. The existing replacement policy unit tests are updated to link the statistics implementation, which is now required to instantiate any policy.

Copy link
Copy Markdown
Contributor

@odanrc odanrc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution, and extra thanks for adding documentation and a unit test!

I brought up the design pattern issue, but it is not that big of a deal - it can be solved in the future, so you can consider it a nitpick if you want.


// LRU calls curTick(), so a current event queue must exist for the policy to
// be exercised. See the fixture below.
EventQueue eventQueue("BaseRPStatsTest Event Queue");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we used LFU we wouldn't need EventQueue's dependency, making the test slightly simpler. This change could also be used to improve the fixture, not requiring makeReplacementPolicy() anymore, allowing us to simply have:

class BaseRPStatsTest : public ::testing::Test
{
  protected:
    TestableLFU rp;
    BaseRPStatsTest() : rp(LFURPParams()) {}
};

{
protected:
/** Profiling information shared by all replacement policies. */
struct ReplacementStats : public statistics::Group
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see 3 possible approaches to take:

  • the "gem5 pattern" (i.e., this proposal, and what we currently do everywhere)
    PROS: It is simple, works; and people are used to it, so adopting it is a no-brainer.
    CONS: Breaks the single responsibility principle (SRP): now RPs take care of RP AND stats.

  • decorator pattern

class StatisticsDecorator : public ReplacementPolicy {
private:
    std::shared_ptr<ReplacementPolicy> wrappedPolicy;
    Stats stats;
    
public:
    void reset(const std::shared_ptr<ReplacementData>& data) override {
        stats.resets++;
        wrapped_policy->reset(data);
    }
    // ... similar for other methods
};

PROS: Simple, maintains SRP, and allow enabling/disabling stats easily. Decorators can even be added one on top of the other to selectively add more stats. We can even add decorators to add tracing! IMO it is the ideal solution.
CONS: Additional learning curve. Has an ugly instantiation (rp = replacement_policy::StatisticsDecorator(LFU()), or something like that), but that drawback can be reduced with a Builder pattern, I think.

  • observer pattern
class ReplacementPolicy {
    std::vector<ReplacementPolicyListener*> listeners;
    
    void touch(const std::shared_ptr<ReplacementData>& data) override {
        for (auto listener : listeners) listener->onTouch();
        touchImpl(data);
    }
};

class StatisticsListener : public ReplacementPolicyListener {
    void onTouch() override { stats.touches++; }
    // ...
};

PROS: allows enabling/disabling stats easily, and we can add listeners for things other than stats, like tracing, and whatever else.
CONS: it still breaks SRP, but it is more complex (yeah, I don't think this is a good solution) ;)

================================================================
All that said, I am ok with the proposed solution - so why do I bring the alternatives up, you may ask? At some point we were discussing enabling/disabling stats (maybe with @giactra ?), and these alternative patterns would be able to cover that. We have the opportunity here to start leaning into the change - but do we want to take it? I think the answer is "no, if we do this we should apply this everywhere, not only here", but at the same time I personally see the RPs as a starting point for change (the first SimObject's unit tests are RP's).

Thoughts?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah... after giving it some thought, I don't think the decorator pattern would work without significant rework of the whole codebase, since it only works well for the boundaries of a function. Since we only care about enabling/disabling stats, this can be done easily through other means, like what we currently do for tracing.

void
invalidate(const std::shared_ptr<ReplacementData> &replacement_data)
{
stats.invalidations++;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: we can't invalidate more than we have inserted, so we could add an assertion for that here

EXPECT_EQ(rp->invalidations(), 1);

// The other counters must be unaffected by unrelated operations.
EXPECT_EQ(rp->victimizations(), 0);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably do the full round of checks for each of the 3 spots here. i.e./e.g., where we currently check that insertions are updated when reset is called, we should also check that the other stats remain unchanged.

# of the 'gem5 simobject' tag. The unit tests below must link them explicitly,
# and the 'socket_test' tag is reused to pull in the zlib (-lz) dependency
# required by base/output.cc.
stats_srcs = [
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not the way to tag files; the proper way to do is to go to each of these files and add ", tags=['gem5 stats']". Check src/base/SConscript for multiple examples. Although from the huge list of files here it looks like we haven't cleaned up the dependencies for the stats yet, so you may have some trouble doing so - I am not sure.

'../../../sim/globals.cc',
'../../../sim/tags.cc',
]
stats_tags = [with_tag('gem5 simobject'), with_tag('socket_test')]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I had the impression that we had with_tags, but I can't recall where these are declared to look it up.

Copy link
Copy Markdown
Contributor

@odanrc odanrc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot to ask: could you re-title this PR to include the "mem-cache:" prefix? You can check other PRs for examples.

{
protected:
/** Profiling information shared by all replacement policies. */
struct ReplacementStats : public statistics::Group
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah... after giving it some thought, I don't think the decorator pattern would work without significant rework of the whole codebase, since it only works well for the boundaries of a function. Since we only care about enabling/disabling stats, this can be done easily through other means, like what we currently do for tracing.

@HirunaVishwamith HirunaVishwamith changed the title Add profiling statistics to replacement policy mem-cache: Add profiling statistics to replacement policy Jun 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants