BigFile/Sasquatch #1

Merged
rboullard merged 100 commits from BigFile into Development 2026-09-01 16:32:02 +00:00
3 changed files with 325 additions and 0 deletions
Showing only changes of commit d3a9121641 - Show all commits
@@ -0,0 +1,92 @@
/*********************************************************************
* \file Graph.cpp
*
* \author Romain BOULLARD
* \date August 2026
*********************************************************************/
#include <Utils/Algorithm/Graph.hpp>
#include <ankerl/unordered_dense.h>
#include <benchmark/benchmark.h>
#include <effolkronium/random.hpp>
#include <cstdint>
namespace Bigfoot
{
using Random = effolkronium::random_static;
class LayeredTopologicalSortFixture: public benchmark::Fixture
{
protected:
/*
* Nodes are grouped into layers, and every node in a layer depends on up to DEPENDENCY_MAX_COUNT
* distinct nodes randomly picked from the previous layer.
*/
void SetUp(benchmark::State& state) override
{
Random::seed(0x5EEDU);
const auto nodeCount = static_cast<std::uint32_t>(state.range(0));
m_graph.clear();
m_nodes.clear();
m_graph.reserve(nodeCount);
m_nodes.reserve(nodeCount);
for (std::uint32_t node = 0; node < nodeCount; ++node)
{
m_nodes.insert(node);
eastl::vector<std::uint32_t>& edges = m_graph[node];
const std::uint32_t layerStart = (node / NODE_COUNT_PER_LAYER) * NODE_COUNT_PER_LAYER;
if (layerStart == 0)
{
continue;
}
const std::uint32_t previousLayerStart = layerStart - NODE_COUNT_PER_LAYER;
ankerl::unordered_dense::set<std::uint32_t> picked;
for (std::uint32_t i = 0; i < DEPENDENCY_MAX_COUNT; ++i)
{
const std::uint32_t dependency = Random::get<std::uint32_t>(previousLayerStart, layerStart - 1);
if (picked.insert(dependency).second)
{
edges.push_back(dependency);
}
}
}
}
static constexpr std::uint32_t NODE_COUNT_PER_LAYER = 64;
static constexpr std::uint32_t DEPENDENCY_MAX_COUNT = 4;
ankerl::unordered_dense::map<std::uint32_t, eastl::vector<std::uint32_t>> m_graph;
ankerl::unordered_dense::set<std::uint32_t> m_nodes;
};
/****************************************************************************************/
BENCHMARK_DEFINE_F(LayeredTopologicalSortFixture, LayeredTopologicalSortBench)(benchmark::State& state)
{
const auto nodeCount = static_cast<std::uint32_t>(state.range(0));
for (auto _: state)
{
eastl::vector<eastl::vector<std::uint32_t>> waves;
eastl::vector<std::uint32_t> cyclicNodes;
benchmark::DoNotOptimize(LayeredTopologicalSort(m_graph, m_nodes, waves, cyclicNodes));
benchmark::DoNotOptimize(waves);
benchmark::ClobberMemory();
}
state.SetItemsProcessed(state.iterations() * nodeCount);
}
BENCHMARK_REGISTER_F(LayeredTopologicalSortFixture, LayeredTopologicalSortBench)
->Name("Bigfoot::LayeredTopologicalSort")
->Arg(512)
->Arg(8192)
->Arg(262'144)
->Arg(4'194'304);
} // namespace Bigfoot
@@ -0,0 +1,154 @@
/*********************************************************************
* \file Graph.hpp
*
* \author Romain BOULLARD
* \date August 2026
*********************************************************************/
#ifndef BIGFOOT_UTILS_ALGORITHM_GRAPH_HPP
#define BIGFOOT_UTILS_ALGORITHM_GRAPH_HPP
#include <Utils/UtilsAssertHandler.hpp>
#include <EASTL/vector.h>
#include <ankerl/unordered_dense.h>
#include <cstdint>
#include <limits>
namespace Bigfoot
{
/*
* Kahn's algorithm
*
* \param p_graph Every discovered node's outgoing edges (eg: its dependencies)
* \param p_nodes The set of nodes to sort
* \param p_waves The sorted nodes
* \param p_cyclicNodes Nodes that could not be sorted (ie: part of a cycle)
* \return True if a cycle was found
*/
template<class NODE_TYPE>
[[nodiscard]]
bool LayeredTopologicalSort(const ankerl::unordered_dense::map<NODE_TYPE, eastl::vector<NODE_TYPE>>& p_graph,
const ankerl::unordered_dense::set<NODE_TYPE>& p_nodes,
eastl::vector<eastl::vector<NODE_TYPE>>& p_waves,
eastl::vector<NODE_TYPE>& p_cyclicNodes)
{
ASSERT(UtilsAssertHandler, p_waves.empty(), "p_waves must be empty");
ASSERT(UtilsAssertHandler, p_cyclicNodes.empty(), "p_cyclicNodes must be empty");
ASSERT(UtilsAssertHandler,
p_nodes.size() <= std::numeric_limits<std::uint32_t>::max(),
"Can't sort more than {} nodes",
std::numeric_limits<std::uint32_t>::max());
const auto nodeCount = static_cast<std::uint32_t>(p_nodes.size());
ankerl::unordered_dense::map<NODE_TYPE, std::uint32_t> nodeToIndex;
nodeToIndex.reserve(nodeCount);
eastl::vector<NODE_TYPE> indexToNode;
indexToNode.reserve(nodeCount);
for (const NODE_TYPE& node: p_nodes)
{
nodeToIndex.emplace(node, static_cast<std::uint32_t>(indexToNode.size()));
indexToNode.push_back(node);
}
// Step 1: Compute in-degree for each node
eastl::vector<std::uint32_t> inDegree(nodeCount, 0);
// Reverse adjacency (which nodes depend on each node) stored as CSR: adjacencyOffsets[i]..adjacencyOffsets[i + 1]
// is node i's slice of adjacency. Built in two passes (count listeners per node, then fill) so the whole
// structure is a couple of flat allocations instead of one per node.
eastl::vector<std::uint32_t> adjacencyOffsets(nodeCount + 1, 0);
// Initialize queue with nodes having in-degree 0
eastl::vector<std::uint32_t> queue;
queue.reserve(nodeCount);
for (std::uint32_t index = 0; index < nodeCount; ++index)
{
std::uint32_t degree = 0;
for (const NODE_TYPE& edge: p_graph.at(indexToNode[index]))
{
const auto it = nodeToIndex.find(edge);
if (it != nodeToIndex.end())
{
++degree;
++adjacencyOffsets[it->second + 1];
}
}
inDegree[index] = degree;
if (degree == 0)
{
queue.push_back(index);
}
}
for (std::uint32_t index = 0; index < nodeCount; ++index)
{
adjacencyOffsets[index + 1] += adjacencyOffsets[index];
}
eastl::vector<std::uint32_t> adjacency(adjacencyOffsets.back());
eastl::vector<std::uint32_t> cursor(adjacencyOffsets.begin(), adjacencyOffsets.end() - 1);
for (std::uint32_t index = 0; index < nodeCount; ++index)
{
for (const NODE_TYPE& edge: p_graph.at(indexToNode[index]))
{
const auto it = nodeToIndex.find(edge);
if (it != nodeToIndex.end())
{
adjacency[cursor[it->second]++] = index;
}
}
}
// Step 2: Process nodes
std::size_t resolvedCount = 0;
eastl::vector<std::uint32_t> nextQueue;
nextQueue.reserve(nodeCount);
while (!queue.empty())
{
resolvedCount += queue.size();
// Reduce in-degree for neighbors
nextQueue.clear();
for (const std::uint32_t index: queue)
{
for (std::uint32_t i = adjacencyOffsets[index]; i < adjacencyOffsets[index + 1]; ++i)
{
const std::uint32_t next = adjacency[i];
if (--inDegree[next] == 0)
{
nextQueue.push_back(next);
}
}
}
eastl::vector<NODE_TYPE> waveNodes;
waveNodes.reserve(queue.size());
for (const std::uint32_t index: queue)
{
waveNodes.push_back(indexToNode[index]);
}
p_waves.push_back(std::move(waveNodes));
queue.swap(nextQueue);
}
// Step 3 & 4: Check for valid topological order
if (resolvedCount != nodeCount)
{
p_cyclicNodes.reserve(nodeCount - resolvedCount);
for (std::uint32_t index = 0; index < nodeCount; ++index)
{
if (inDegree[index] != 0)
{
p_cyclicNodes.push_back(indexToNode[index]);
}
}
}
return resolvedCount != nodeCount;
}
} // namespace Bigfoot
#endif
+79
View File
@@ -0,0 +1,79 @@
/*********************************************************************
* \file Graph.cpp
*
* \author Romain BOULLARD
* \date August 2026
*********************************************************************/
#include <Utils/Algorithm/Graph.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
namespace Bigfoot
{
class LayeredTopologicalSortFixture: public ::testing::Test
{
protected:
ankerl::unordered_dense::map<std::uint32_t, eastl::vector<std::uint32_t>> m_graph;
ankerl::unordered_dense::set<std::uint32_t> m_nodes;
eastl::vector<eastl::vector<std::uint32_t>> m_waves;
eastl::vector<std::uint32_t> m_cyclicNodes;
};
/****************************************************************************************/
TEST_F(LayeredTopologicalSortFixture, ShouldOrderEachNodeInAnEarlierWaveThanTheNodesThatDependOnIt)
{
m_graph = {{0, {1, 2}}, {1, {3}}, {2, {3}}, {3, {}}};
m_nodes = {0, 1, 2, 3};
EXPECT_FALSE(LayeredTopologicalSort(m_graph, m_nodes, m_waves, m_cyclicNodes));
ASSERT_EQ(m_waves.size(), 3);
EXPECT_THAT(m_waves[0], ::testing::UnorderedElementsAre(3));
EXPECT_THAT(m_waves[1], ::testing::UnorderedElementsAre(1, 2));
EXPECT_THAT(m_waves[2], ::testing::UnorderedElementsAre(0));
EXPECT_TRUE(m_cyclicNodes.empty());
}
/****************************************************************************************/
TEST_F(LayeredTopologicalSortFixture, ShouldOmitNodesThatAreCaughtInACycleWithoutHanging)
{
m_graph = {{0, {1}}, {1, {0}}};
m_nodes = {0, 1};
EXPECT_TRUE(LayeredTopologicalSort(m_graph, m_nodes, m_waves, m_cyclicNodes));
EXPECT_TRUE(m_waves.empty());
EXPECT_THAT(m_cyclicNodes, ::testing::UnorderedElementsAre(0, 1));
}
/****************************************************************************************/
TEST_F(LayeredTopologicalSortFixture, ShouldIgnoreEdgesThatPointOutsideOfTheNodesBeingSorted)
{
m_graph = {{0, {1}}};
m_nodes = {0};
EXPECT_FALSE(LayeredTopologicalSort(m_graph, m_nodes, m_waves, m_cyclicNodes));
ASSERT_EQ(m_waves.size(), 1);
EXPECT_THAT(m_waves[0], ::testing::ElementsAre(0));
EXPECT_TRUE(m_cyclicNodes.empty());
}
/****************************************************************************************/
TEST_F(LayeredTopologicalSortFixture, ShouldReportNodesThatOnlyDependOnACycleAsCyclicToo)
{
// 0 <-> 1 is the cycle; 2 depends on 0 so it can never resolve either, even though it isn't part of the cycle.
m_graph = {{0, {1}}, {1, {0}}, {2, {0}}};
m_nodes = {0, 1, 2};
EXPECT_TRUE(LayeredTopologicalSort(m_graph, m_nodes, m_waves, m_cyclicNodes));
EXPECT_TRUE(m_waves.empty());
EXPECT_THAT(m_cyclicNodes, ::testing::UnorderedElementsAre(0, 1, 2));
}
} // namespace Bigfoot