Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ core_SOURCES = \
src/core/core.h \
src/core/util.h \
src/core/array.h \
src/core/lazy.h \
src/core/vector.h \
src/core/segment.h \
src/core/recarray.h \
Expand Down
149 changes: 149 additions & 0 deletions src/core/lazy.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
//
// This file is part of Gambit
// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org)
//
// FILE: src/core/lazy.h
// Lazily-computed, invalidatable cached values and setup actions
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
//

#ifndef GAMBIT_CORE_LAZY_H
#define GAMBIT_CORE_LAZY_H

#include <mutex>
#include <optional>
#include <utility>

namespace Gambit {

/// @brief A lazily-computed value, rebuilt on demand after invalidation.
///
/// Get() runs `p_builder` at most once since construction or the last
/// Invalidate() (std::call_once semantics), then returns the cached value on
/// every subsequent call without re-running the builder.
///
/// Concurrent Get() calls race safely: the standard guarantees exactly one
/// builder call completes before any Get() call returns, so multiple reader
/// threads first-touching the same Lazy<T> is well-defined. Invalidate() is
/// NOT safe to call concurrently with Get(); invalidation must happen only
/// when no reader could be in flight (e.g. before handing the owning object
/// to a reader thread, never while one is active).
///
/// Copyable, for embedding in value-type classes that are themselves copied
/// (e.g. a support object held by value inside a profile class) — but a copy
/// always starts un-built, regardless of the source's state. There is no
/// way to safely transplant an already-fired std::once_flag, and a fast-path
/// check of the source's cached value outside of call_once would reintroduce
/// the exact data race call_once exists to prevent. Concretely: copying an
/// already-built Lazy<T> does not carry the built value over; the copy
/// recomputes it lazily on its own next Get().
template <class T> class Lazy {
public:
Lazy() = default;
Lazy(const Lazy &) {}
Lazy &operator=(const Lazy &)
{
Invalidate();
return *this;
}

/// Get the cached value, computing it via `p_builder` if not already built
/// (or if invalidated since the last build).
template <class F> const T &Get(F &&p_builder) const
{
std::call_once(*m_flag, [&] { m_value = std::forward<F>(p_builder)(); });
return *m_value;
}

/// Discard the cached value; the next Get() call recomputes it.
void Invalidate() const
{
m_flag.emplace();
m_value.reset();
}

/// If a value has been built, apply `p_visitor` to it; a no-op otherwise.
/// Useful for releasing resources the cached value owns (e.g. cascading to
/// invalidate objects it handed out) immediately before discarding it via
/// Invalidate(), without forcing a rebuild just to inspect the old value.
template <class F> void IfBuilt(F &&p_visitor) const
{
if (m_value) {
std::forward<F>(p_visitor)(*m_value);
}
}

/// Whether the value has been computed since the last invalidation.
bool IsBuilt() const { return m_value.has_value(); }

private:
// std::once_flag is neither copyable nor movable, so it is wrapped in
// std::optional and reset via emplace() (placement-construct a fresh
// flag) rather than assignment.
mutable std::optional<std::once_flag> m_flag{std::in_place};
mutable std::optional<T> m_value;
};

/// @brief Like Lazy<T>, but for an idempotent setup action with no single
/// value to hand back.
///
/// Some caches are populated by a routine that mutates several existing
/// members as a side effect (e.g. numbering nodes in place) rather than
/// producing one value to store. LazyAction captures the same "run once
/// since the last invalidation" semantics for that shape, sharing Lazy<T>'s
/// concurrency guarantees and its restriction against invalidating
/// concurrently with a call to Ensure().
///
/// Copyable, with the same "a copy always starts un-built" semantics as
/// Lazy<T> and for the same reason (see above).
class LazyAction {
public:
LazyAction() = default;
LazyAction(const LazyAction &) {}
LazyAction &operator=(const LazyAction &)
{
Invalidate();
return *this;
}

/// Run `p_action` if it has not already run since construction or the
/// last Invalidate().
template <class F> void Ensure(F &&p_action) const
{
std::call_once(*m_flag, [&] {
std::forward<F>(p_action)();
m_built = true;
});
}

/// Mark the action as needing to run again on the next Ensure() call.
void Invalidate() const
{
m_flag.emplace();
m_built = false;
}

/// Whether the action has run since the last invalidation.
bool IsBuilt() const { return m_built; }

private:
mutable std::optional<std::once_flag> m_flag{std::in_place};
mutable bool m_built{false};
};

} // namespace Gambit

#endif // GAMBIT_CORE_LAZY_H
6 changes: 3 additions & 3 deletions src/games/behavmixed.cc
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ template <class T> MixedBehaviorProfile<T> MixedBehaviorProfile<T>::ToFullSuppor

template <class T> T MixedBehaviorProfile<T>::GetLiapValue() const
{
m_support.GetGame()->BuildComputedValues();
m_support.GetGame()->EnsureStrategies();
return MixedStrategyProfile<T>(*this).GetLiapValue();
}

Expand Down Expand Up @@ -402,7 +402,7 @@ template <class T> T MixedBehaviorProfile<T>::GetRegret(const GameInfoset &p_inf

template <class T> T MixedBehaviorProfile<T>::GetMaxRegret() const
{
m_support.GetGame()->BuildComputedValues();
m_support.GetGame()->EnsureStrategies();
return MixedStrategyProfile<T>(*this).GetMaxRegret();
}

Expand Down Expand Up @@ -619,7 +619,7 @@ template <class T> bool MixedBehaviorProfile<T>::IsDefinedAt(GameInfoset p_infos
template <class T> MixedStrategyProfile<T> MixedBehaviorProfile<T>::ToMixedProfile() const
{
CheckVersion();
m_support.GetGame()->BuildComputedValues();
m_support.GetGame()->EnsureStrategies();
return MixedStrategyProfile<T>(*this);
}

Expand Down
39 changes: 20 additions & 19 deletions src/games/behavspt.cc
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ size_t BehaviorSupportProfile::BehaviorProfileLength() const

void BehaviorSupportProfile::AddAction(const GameAction &p_action)
{
m_reachableInfosets = nullptr;
m_sequences = nullptr;
m_reachableInfosets.Invalidate();
m_sequences.Invalidate();
auto &support = m_actions.at(p_action->GetInfoset());
auto pos = std::find_if(support.begin(), support.end(), [p_action](const GameAction &a) {
return a->GetNumber() >= p_action->GetNumber();
Expand All @@ -83,8 +83,8 @@ void BehaviorSupportProfile::AddAction(const GameAction &p_action)

bool BehaviorSupportProfile::RemoveAction(const GameAction &p_action)
{
m_reachableInfosets = nullptr;
m_sequences = nullptr;
m_reachableInfosets.Invalidate();
m_sequences.Invalidate();
auto &support = m_actions.at(p_action->GetInfoset());
auto pos = std::find(support.begin(), support.end(), p_action);
if (pos != support.end()) {
Expand Down Expand Up @@ -150,17 +150,17 @@ void BehaviorSupportProfile::DeactivateSubtree(const GameNode &n)

std::shared_ptr<BehaviorSupportProfile::SequenceMap> BehaviorSupportProfile::GetSequenceMap() const
{
if (!m_sequences) {
m_sequences = std::make_shared<SequenceMap>();
return m_sequences.Get([&] {
auto sequences = std::make_shared<SequenceMap>();
for (const auto &player : GetGame()->GetPlayers()) {
for (const auto &sequence : player->GetSequences()) {
if (!sequence->GetAction() || Contains(sequence->GetAction())) {
(*m_sequences)[player].emplace_back(sequence);
(*sequences)[player].emplace_back(sequence);
}
}
}
}
return m_sequences;
return sequences;
});
}

BehaviorSupportProfile::Sequences BehaviorSupportProfile::GetSequences() const { return {this}; }
Expand Down Expand Up @@ -290,37 +290,38 @@ size_t BehaviorSupportProfile::PlayerSequences::size() const
// BehaviorSupportProfile: Reachable Information Sets
//========================================================================

void BehaviorSupportProfile::FindReachableInfosets(GameNode p_node) const
void BehaviorSupportProfile::FindReachableInfosets(GameNode p_node,
std::map<GameInfoset, bool> &p_reachable) const
{
if (!p_node->IsTerminal()) {
auto infoset = p_node->GetInfoset();
(*m_reachableInfosets)[infoset] = true;
p_reachable[infoset] = true;
if (p_node->GetPlayer()->IsChance()) {
for (auto action : infoset->GetActions()) {
FindReachableInfosets(p_node->GetChild(action));
FindReachableInfosets(p_node->GetChild(action), p_reachable);
}
}
else {
for (auto action : GetActions(infoset)) {
FindReachableInfosets(p_node->GetChild(action));
FindReachableInfosets(p_node->GetChild(action), p_reachable);
}
}
}
}

std::shared_ptr<std::map<GameInfoset, bool>> BehaviorSupportProfile::GetReachableInfosets() const
{
if (!m_reachableInfosets) {
m_reachableInfosets = std::make_shared<std::map<GameInfoset, bool>>();
return m_reachableInfosets.Get([&] {
auto reachable = std::make_shared<std::map<GameInfoset, bool>>();
for (size_t pl = 0; pl <= GetGame()->NumPlayers(); pl++) {
const GamePlayer player = (pl == 0) ? GetGame()->GetChance() : GetGame()->GetPlayer(pl);
for (const auto &infoset : player->GetInfosets()) {
(*m_reachableInfosets)[infoset] = false;
(*reachable)[infoset] = false;
}
}
FindReachableInfosets(GetGame()->GetRoot());
}
return m_reachableInfosets;
FindReachableInfosets(GetGame()->GetRoot(), *reachable);
return reachable;
});
}

} // end namespace Gambit
7 changes: 4 additions & 3 deletions src/games/behavspt.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

#include <list>
#include <map>
#include "core/lazy.h"
#include "game.h"
#include "seqpure.h"

Expand All @@ -46,8 +47,8 @@ class BehaviorSupportProfile {
private:
Game m_efg;
std::map<GameInfoset, std::vector<GameAction>> m_actions;
mutable std::shared_ptr<SequenceMap> m_sequences;
mutable std::shared_ptr<std::map<GameInfoset, bool>> m_reachableInfosets;
mutable Lazy<std::shared_ptr<SequenceMap>> m_sequences;
mutable Lazy<std::shared_ptr<std::map<GameInfoset, bool>>> m_reachableInfosets;

std::map<GameInfoset, bool> m_infosetReachable;
std::map<GameNode, bool> m_nonterminalReachable;
Expand All @@ -56,6 +57,7 @@ class BehaviorSupportProfile {
void ActivateSubtree(const GameNode &);
void DeactivateSubtree(const GameNode &);
std::shared_ptr<SequenceMap> GetSequenceMap() const;
void FindReachableInfosets(GameNode p_node, std::map<GameInfoset, bool> &p_reachable) const;

public:
class Support {
Expand Down Expand Up @@ -208,7 +210,6 @@ class BehaviorSupportProfile {
Infosets GetInfosets() const { return {this}; };
SequenceContingencies GetSequenceContingencies() const;

void FindReachableInfosets(GameNode p_node) const;
std::shared_ptr<std::map<GameInfoset, bool>> GetReachableInfosets() const;
};

Expand Down
10 changes: 5 additions & 5 deletions src/games/game.h
Original file line number Diff line number Diff line change
Expand Up @@ -1174,7 +1174,7 @@ class GameRep : public std::enable_shared_from_this<GameRep> {
/// Returns the set of strategies in the game
Strategies GetStrategies() const
{
BuildComputedValues();
EnsureStrategies();
return Strategies(std::const_pointer_cast<GameRep>(this->shared_from_this()));
}
/// Gets the i'th strategy in the game, numbered globally starting from 1
Expand Down Expand Up @@ -1267,8 +1267,8 @@ class GameRep : public std::enable_shared_from_this<GameRep> {
virtual Game SetChanceProbs(const GameInfoset &, const Array<Number> &) = 0;
//@}

/// Build any computed values anew
virtual void BuildComputedValues() const {}
/// Ensure the reduced-form strategies have been derived and indexed
virtual void EnsureStrategies() const {}
/// Ensure sequences have been computed
virtual void EnsureSequences() const { throw UndefinedException(); }

Expand Down Expand Up @@ -1416,12 +1416,12 @@ inline void GamePlayerRep::SetLabel(const std::string &p_label)
}
inline GameStrategy GamePlayerRep::GetStrategy(int st) const
{
m_game->BuildComputedValues();
m_game->EnsureStrategies();
return m_strategies.at(st - 1);
}
inline GamePlayerRep::Strategies GamePlayerRep::GetStrategies() const
{
m_game->BuildComputedValues();
m_game->EnsureStrategies();
return Strategies(std::const_pointer_cast<GamePlayerRep>(shared_from_this()), &m_strategies);
}
inline GamePlayerRep::Sequences GamePlayerRep::GetSequences() const
Expand Down
Loading
Loading