Use a LRU cache for pip to wire map.

This avoids storing the entire pip to wire map in memory with a moderate
runtime increase and a dramatic memory decrease (2 GB to 200 MB for
A50T).

Signed-off-by: Keith Rothman <537074+litghost@users.noreply.github.com>
This commit is contained in:
Keith Rothman 2021-02-03 13:45:47 -08:00
parent 155e0b9c42
commit 235a8e07e3

View File

@ -131,14 +131,114 @@ void archcheck_locs(const Context *ctx)
log_break(); log_break();
} }
// Implements a LRU cache for pip to wire via getPipsDownhill/getPipsUphill.
//
// This allows a fast way to check getPipsDownhill/getPipsUphill from getPips,
// without balloning memory usage.
struct LruWireCacheMap {
LruWireCacheMap(const Context *ctx, size_t cache_size) : ctx(ctx), cache_size(cache_size) {
cache_hits = 0;
cache_misses = 0;
cache_evictions = 0;
}
const Context *ctx;
size_t cache_size;
// Cache stats for checking on cache behavior.
size_t cache_hits;
size_t cache_misses;
size_t cache_evictions;
// Most recent accessed wires are added to the back of the list, front of
// list is oldest wire in cache.
std::list<WireId> last_access_list;
// Quick wire -> list element lookup.
std::unordered_map<WireId, std::list<WireId>::iterator> last_access_map;
std::unordered_map<PipId, WireId> pips_downhill;
std::unordered_map<PipId, WireId> pips_uphill;
void removeWireFromCache(WireId wire_to_remove) {
for (PipId pip : ctx->getPipsDownhill(wire_to_remove)) {
log_assert(pips_downhill.erase(pip) == 1);
}
for (PipId pip : ctx->getPipsUphill(wire_to_remove)) {
log_assert(pips_uphill.erase(pip) == 1);
}
}
void addWireToCache(WireId wire) {
for (PipId pip : ctx->getPipsDownhill(wire)) {
auto result = pips_downhill.emplace(pip, wire);
log_assert(result.second);
}
for (PipId pip : ctx->getPipsUphill(wire)) {
auto result = pips_uphill.emplace(pip, wire);
log_assert(result.second);
}
}
void populateCache(WireId wire) {
// Put this wire at the end of last_access_list.
auto iter = last_access_list.emplace(last_access_list.end(), wire);
last_access_map.emplace(wire, iter);
if(last_access_list.size() > cache_size) {
// Cache is full, remove front of last_access_list.
cache_evictions += 1;
WireId wire_to_remove = last_access_list.front();
last_access_list.pop_front();
log_assert(last_access_map.erase(wire_to_remove) == 1);
removeWireFromCache(wire_to_remove);
}
addWireToCache(wire);
}
// Determine if wire is in the cache. If wire is not in the cache,
// adds the wire to the cache, and potentially evicts the oldest wire if
// cache is now full.
void checkCache(WireId wire) {
auto iter = last_access_map.find(wire);
if(iter == last_access_map.end()) {
cache_misses += 1;
populateCache(wire);
} else {
// Record that this wire has been accessed.
cache_hits += 1;
last_access_list.splice(last_access_list.end(), last_access_list, iter->second);
}
}
// Returns true if pip is uphill of wire (e.g. pip in getPipsUphill(wire)).
bool isPipUphill(PipId pip, WireId wire) {
checkCache(wire);
return pips_uphill.at(pip) == wire;
}
// Returns true if pip is downhill of wire (e.g. pip in getPipsDownhill(wire)).
bool isPipDownhill(PipId pip, WireId wire) {
checkCache(wire);
return pips_downhill.at(pip) == wire;
}
void cache_info() const {
log_info("Cache hits: %zu\n", cache_hits);
log_info("Cache misses: %zu\n", cache_misses);
log_info("Cache evictions: %zu\n", cache_evictions);
}
};
void archcheck_conn(const Context *ctx) void archcheck_conn(const Context *ctx)
{ {
log_info("Checking connectivity data.\n"); log_info("Checking connectivity data.\n");
log_info("Checking all wires...\n"); log_info("Checking all wires...\n");
std::unordered_map<PipId, WireId> pips_downhill;
std::unordered_map<PipId, WireId> pips_uphill;
for (WireId wire : ctx->getWires()) { for (WireId wire : ctx->getWires()) {
for (BelPin belpin : ctx->getWireBelPins(wire)) { for (BelPin belpin : ctx->getWireBelPins(wire)) {
WireId wire2 = ctx->getBelPinWire(belpin.bel, belpin.pin); WireId wire2 = ctx->getBelPinWire(belpin.bel, belpin.pin);
@ -148,17 +248,11 @@ void archcheck_conn(const Context *ctx)
for (PipId pip : ctx->getPipsDownhill(wire)) { for (PipId pip : ctx->getPipsDownhill(wire)) {
WireId wire2 = ctx->getPipSrcWire(pip); WireId wire2 = ctx->getPipSrcWire(pip);
log_assert(wire == wire2); log_assert(wire == wire2);
auto result = pips_downhill.emplace(pip, wire);
log_assert(result.second);
} }
for (PipId pip : ctx->getPipsUphill(wire)) { for (PipId pip : ctx->getPipsUphill(wire)) {
WireId wire2 = ctx->getPipDstWire(pip); WireId wire2 = ctx->getPipDstWire(pip);
log_assert(wire == wire2); log_assert(wire == wire2);
auto result = pips_uphill.emplace(pip, wire);
log_assert(result.second);
} }
} }
@ -183,16 +277,25 @@ void archcheck_conn(const Context *ctx)
} }
} }
// This cache is used to meet two goals:
// - Avoid linear scan by invoking getPipsDownhill/getPipsUphill directly.
// - Avoid having pip -> wire maps for the entire part.
//
// The overhead of maintaining the cache is small relatively to the memory
// gains by avoiding the full pip -> wire map, and still preserves a fast
// pip -> wire, assuming that pips are returned from getPips with some
// chip locality.
LruWireCacheMap pip_cache(ctx, /*cache_size=*/64*1024);
log_info("Checking all PIPs...\n"); log_info("Checking all PIPs...\n");
for (PipId pip : ctx->getPips()) { for (PipId pip : ctx->getPips()) {
WireId src_wire = ctx->getPipSrcWire(pip); WireId src_wire = ctx->getPipSrcWire(pip);
if (src_wire != WireId()) { if (src_wire != WireId()) {
log_assert(pips_downhill.at(pip) == src_wire); log_assert(pip_cache.isPipDownhill(pip, src_wire));
} }
WireId dst_wire = ctx->getPipDstWire(pip); WireId dst_wire = ctx->getPipDstWire(pip);
if (dst_wire != WireId()) { if (dst_wire != WireId()) {
log_assert(pips_uphill.at(pip) == dst_wire); log_assert(pip_cache.isPipUphill(pip, dst_wire));
} }
} }
} }