Files
libretroadmin 75d3cc5d78 GS/HW: the hash cache is a C hash map whose entries stay put
The texture cache's hash cache was a std::unordered_map, with nodes
from GSNodeAllocator's pool. It becomes a GSStableMap, new in C89. Sources
point at their entries, so an entry can't move the way a GSFlatMap's
values do. Here the slots are a stored hash and a node pointer, sixteen
bytes each, at most a quarter in use, with linear probing. Each node
holds its key next to its value, so a hit reads the slot and the node.
A node knows its slot and its place in a dense list of nodes, so an
erase neither looks it up nor hashes it again (backward shift for the
slots, the last node into its place in the list). A walk goes down the
list and can erase the entry it is at. AgeHashCache does that instead of
erasing through an iterator. Its purge list is a plain array of entries,
grown only when the cache is fuller than it has been. The slots for 800
entries are made when the cache is.

tests/gsstablemap checks the map against a model, with nodes from a
pool and from the allocator, including erases during a walk. With
GSSM_BENCH=1 it times both maps on 40-byte keys at 32 to 800 entries,
best of five, ns per operation, std then stable:

  find hit      9-13  vs  6-9    (1.3-1.6x)
  find miss     9-11  vs  7-8    (1.1-1.5x)
  walk          1.1-3.8 vs 0.6-1.3 per entry (1.2-2.9x)
  insert+erase  20-29 vs 15-20   (1.3-1.5x)

Within a few percent of the quarter-load boundary the miss and insert
times are even. Ridge Racer V's intro presents the same 19 frames, bit
for bit, and the steady-state allocation count stays at zero.
2026-11-02 20:04:39 +01:00

221 lines
6.1 KiB
C++

/* The texture cache's hash cache map, timed both ways: the
* std::unordered_map it was (GSNodeAllocator's node pool, gs_key_hash)
* and GSStableMap. 40-byte keys, 24-byte values, the map at a few sizes
* up to the cache's 800 entries. Per operation:
* - find, hit: what every draw with a cached texture does, up to three
* times;
* - find, miss: a texture not cached yet;
* - the per-frame walk, aging each entry (AgeHashCache);
* - insert then erase: a texture made and purged.
* Prints ns per operation for each, the best of five runs, and the
* ratio. */
#include <chrono>
#include <cstdio>
#include <cstring>
#include <new>
#include <unordered_map>
#include <vector>
#include "GS/Renderers/Common/GSKeyHash.h"
#include "GS/Renderers/Common/GSStableMap.h"
struct Key
{
unsigned long long TEX0Hash, CLUTHash, TEX0, TEXA;
unsigned region_width, region_height;
bool operator==(const Key& e) const { return std::memcmp(this, &e, sizeof(*this)) == 0; }
};
struct KeyHash
{
unsigned long long operator()(const Key& k) const { return gs_key_hash(&k, sizeof(k)); }
};
struct Entry
{
void* texture;
unsigned refcount;
unsigned short age;
unsigned char alpha[2];
bool valid, replacement;
};
/* GSTextureCache.h's, as it is there. */
template <typename T>
class NodeAllocator
{
static constexpr bool POOLED = !GS_OBJECT_POOL_SANITIZED && alignof(T) <= GS_OBJECT_POOL_ALIGN;
public:
using value_type = T;
NodeAllocator() noexcept = default;
template <typename U>
NodeAllocator(const NodeAllocator<U>&) noexcept {}
T* allocate(std::size_t n)
{
if (POOLED && n == 1)
return static_cast<T*>(gs_node_pool_take(&s_pool));
return static_cast<T*>(::operator new(n * sizeof(T)));
}
void deallocate(T* p, std::size_t n) noexcept
{
if (POOLED && n == 1)
gs_node_pool_give(&s_pool, p);
else
::operator delete(p);
}
template <typename U>
bool operator==(const NodeAllocator<U>&) const noexcept { return true; }
template <typename U>
bool operator!=(const NodeAllocator<U>&) const noexcept { return false; }
private:
static inline gs_node_pool_t s_pool = GS_NODE_POOL_INIT(sizeof(T), alignof(T));
};
using StdMap = std::unordered_map<Key, Entry, KeyHash, std::equal_to<Key>, NodeAllocator<std::pair<const Key, Entry>>>;
static gs_node_pool_t s_nodes = GS_NODE_POOL_INIT(GS_STABLE_MAP_NODE_SIZE(sizeof(Key), sizeof(Entry)), 16);
static Key make_key(unsigned i)
{
Key k;
std::memset(&k, 0, sizeof(k));
k.TEX0Hash = 0x9e3779b97f4a7c15ull * (i + 1);
k.CLUTHash = (i & 3) ? 0 : 0xabcdef12345ull * i;
k.TEX0 = (unsigned long long)(i & 0x3ff) << 20;
k.region_width = i & 7;
return k;
}
static volatile unsigned long long s_sink;
/* The best of five: a run the scheduler interrupted says nothing. */
#define BEST(out, ops, body) \
do \
{ \
out = 1e30; \
for (int run_ = 0; run_ < 5; run_++) \
{ \
auto t_ = std::chrono::steady_clock::now(); \
body; \
const double ns_ = ns_since(t_, ops); \
if (ns_ < out) \
out = ns_; \
} \
} while (0)
static double ns_since(std::chrono::steady_clock::time_point t, double ops)
{
return std::chrono::duration<double, std::nano>(std::chrono::steady_clock::now() - t).count() / ops;
}
int main()
{
const unsigned sizes[] = {32, 64, 100, 256, 400, 800};
const unsigned rounds = 2000000;
std::printf("%-6s %-12s %10s %10s %7s\n", "size", "op", "std ns", "stable ns", "ratio");
for (unsigned size : sizes)
{
std::vector<Key> keys, misses;
std::vector<unsigned> order(4096);
for (unsigned i = 0; i < size; i++)
keys.push_back(make_key(i));
for (unsigned i = 0; i < 4096; i++)
misses.push_back(make_key(100000 + i));
/* Lookups in an order no prefetcher follows. */
unsigned long long r = 88172645463325252ull;
for (unsigned& o : order)
{
r ^= r << 13; r ^= r >> 7; r ^= r << 17;
o = (unsigned)(r % size);
}
StdMap sm;
gs_stable_map_t gm;
gs_stable_map_init(&gm, sizeof(Key), sizeof(Entry), &s_nodes);
for (const Key& k : keys)
{
int ins;
sm.emplace(k, Entry{});
gs_stable_map_insert(&gm, &k, gs_key_hash(&k, sizeof(k)), &ins);
}
double t_std, t_gs;
unsigned long long acc = 0;
/* find, hit */
BEST(t_std, rounds, {
for (unsigned i = 0; i < rounds; i++)
{
auto it = sm.find(keys[order[i & 4095]]);
acc += ++it->second.refcount;
}
});
BEST(t_gs, rounds, {
for (unsigned i = 0; i < rounds; i++)
{
const Key& k = keys[order[i & 4095]];
Entry* e = (Entry*)gs_stable_map_find(&gm, &k, sizeof(Key), gs_key_hash(&k, sizeof(k)));
acc += ++e->refcount;
}
});
std::printf("%-6u %-12s %10.2f %10.2f %7.2f\n", size, "find hit", t_std, t_gs, t_std / t_gs);
/* find, miss */
BEST(t_std, rounds, {
for (unsigned i = 0; i < rounds; i++)
acc += sm.find(misses[i & 4095]) == sm.end();
});
BEST(t_gs, rounds, {
for (unsigned i = 0; i < rounds; i++)
{
const Key& k = misses[i & 4095];
acc += gs_stable_map_find(&gm, &k, sizeof(Key), gs_key_hash(&k, sizeof(k))) == NULL;
}
});
std::printf("%-6u %-12s %10.2f %10.2f %7.2f\n", size, "find miss", t_std, t_gs, t_std / t_gs);
/* the per-frame walk */
const unsigned walks = rounds / size;
BEST(t_std, (double)walks * size, {
for (unsigned w = 0; w < walks; w++)
for (auto& kv : sm)
if (kv.second.refcount == 0 || ++kv.second.age)
acc++;
});
BEST(t_gs, (double)walks * size, {
for (unsigned w = 0; w < walks; w++)
for (size_t s = GS_STABLE_MAP_COUNT(&gm); s-- > 0;)
{
Entry* e = (Entry*)gs_stable_map_at(&gm, s);
if (e->refcount == 0 || ++e->age)
acc++;
}
});
std::printf("%-6u %-12s %10.2f %10.2f %7.2f\n", size, "walk/entry", t_std, t_gs, t_std / t_gs);
/* insert then erase */
BEST(t_std, rounds / 4, {
for (unsigned i = 0; i < rounds / 4; i++)
{
const Key& k = misses[i & 4095];
auto it = sm.emplace(k, Entry{}).first;
sm.erase(it);
}
});
BEST(t_gs, rounds / 4, {
for (unsigned i = 0; i < rounds / 4; i++)
{
const Key& k = misses[i & 4095];
int ins;
void* v = gs_stable_map_insert(&gm, &k, gs_key_hash(&k, sizeof(k)), &ins);
gs_stable_map_erase(&gm, v);
}
});
std::printf("%-6u %-12s %10.2f %10.2f %7.2f\n", size, "insert+erase", t_std, t_gs, t_std / t_gs);
s_sink = acc;
gs_stable_map_free(&gm);
}
return 0;
}