raze/source/common/objects/dobjgc.cpp
Christoph Oelckers 44e64a6a12 - backend update from GZDoom.
* GC fix
* better sound range check
* UE model loader license change.
2022-01-11 22:54:37 +01:00

673 lines
18 KiB
C++

/*
** dobjgc.cpp
** The garbage collector. Based largely on Lua's.
**
**---------------------------------------------------------------------------
** Copyright 2008-2022 Marisa Heit
** All rights reserved.
**
** Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions
** are met:
**
** 1. Redistributions of source code must retain the above copyright
** notice, this list of conditions and the following disclaimer.
** 2. Redistributions in binary form must reproduce the above copyright
** notice, this list of conditions and the following disclaimer in the
** documentation and/or other materials provided with the distribution.
** 3. The name of the author may not be used to endorse or promote products
** derived from this software without specific prior written permission.
**
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**---------------------------------------------------------------------------
**
*/
/******************************************************************************
* Copyright (C) 1994-2008 Lua.org, PUC-Rio. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************************************************************/
// HEADER FILES ------------------------------------------------------------
#include "dobject.h"
#include "c_dispatch.h"
#include "menu.h"
#include "stats.h"
#include "printf.h"
// MACROS ------------------------------------------------------------------
/*
@@ DEFAULT_GCPAUSE defines the default pause between garbage-collector cycles
@* as a percentage.
** CHANGE it if you want the GC to run faster or slower (higher values
** mean larger pauses which mean slower collection.) You can also change
** this value dynamically.
*/
#define DEFAULT_GCPAUSE 150 // 150% (wait for memory to increase by half before next GC)
/*
@@ DEFAULT_GCMUL defines the default speed of garbage collection relative to
@* memory allocation as a percentage.
** CHANGE it if you want to change the granularity of the garbage
** collection. (Higher values mean coarser collections. 0 represents
** infinity, where each step performs a full collection.) You can also
** change this value dynamically.
*/
#define DEFAULT_GCMUL 200 // GC runs 'double the speed' of memory allocation
// Minimum step size
#define GCSTEPSIZE (sizeof(DObject) * 16)
// Maximum number of elements to sweep in a single step
#define GCSWEEPMAX 40
// Cost of sweeping one element (the size of a small object divided by
// some adjust for the sweep speed)
#define GCSWEEPCOST (sizeof(DObject) / 4)
// Cost of calling of one destructor
#define GCFINALIZECOST 100
// TYPES -------------------------------------------------------------------
// EXTERNAL FUNCTION PROTOTYPES --------------------------------------------
// PUBLIC FUNCTION PROTOTYPES ----------------------------------------------
// PRIVATE FUNCTION PROTOTYPES ---------------------------------------------
static size_t CalcStepSize();
// EXTERNAL DATA DECLARATIONS ----------------------------------------------
// PUBLIC DATA DEFINITIONS -------------------------------------------------
namespace GC
{
size_t AllocBytes;
size_t Threshold;
size_t Estimate;
DObject *Gray;
DObject *Root;
DObject *SoftRoots;
DObject **SweepPos;
uint32_t CurrentWhite = OF_White0 | OF_Fixed;
EGCState State = GCS_Pause;
int Pause = DEFAULT_GCPAUSE;
int StepMul = DEFAULT_GCMUL;
int StepCount;
uint64_t CheckTime;
bool FinalGC;
// PRIVATE DATA DEFINITIONS ------------------------------------------------
static int LastCollectTime; // Time last time collector finished
static size_t LastCollectAlloc; // Memory allocation when collector finished
static size_t MinStepSize; // Cover at least this much memory per step
// CODE --------------------------------------------------------------------
//==========================================================================
//
// SetThreshold
//
// Sets the new threshold after a collection is finished.
//
//==========================================================================
void SetThreshold()
{
Threshold = (Estimate / 100) * Pause;
}
//==========================================================================
//
// PropagateMark
//
// Marks the top-most gray object black and marks all objects it points to
// gray.
//
//==========================================================================
size_t PropagateMark()
{
DObject *obj = Gray;
assert(obj->IsGray());
obj->Gray2Black();
Gray = obj->GCNext;
return !(obj->ObjectFlags & OF_EuthanizeMe) ? obj->PropagateMark() :
obj->GetClass()->Size;
}
//==========================================================================
//
// SweepList
//
// Runs a limited sweep on a list, returning the position in the list just
// after the last object swept.
//
//==========================================================================
static DObject **SweepList(DObject **p, size_t count, size_t *finalize_count)
{
DObject *curr;
int deadmask = OtherWhite();
size_t finalized = 0;
while ((curr = *p) != NULL && count-- > 0)
{
if ((curr->ObjectFlags ^ OF_WhiteBits) & deadmask) // not dead?
{
assert(!curr->IsDead() || (curr->ObjectFlags & OF_Fixed));
curr->MakeWhite(); // make it white (for next cycle)
p = &curr->ObjNext;
}
else // must erase 'curr'
{
assert(curr->IsDead());
*p = curr->ObjNext;
if (!(curr->ObjectFlags & OF_EuthanizeMe))
{ // The object must be destroyed before it can be finalized.
// Note that thinkers must already have been destroyed. If they get here without
// having been destroyed first, it means they somehow became unattached from the
// thinker lists. If I don't maintain the invariant that all live thinkers must
// be in a thinker list, then I need to add write barriers for every time a
// thinker pointer is changed. This seems easier and perfectly reasonable, since
// a live thinker that isn't on a thinker list isn't much of a thinker.
// However, this can happen during deletion of the thinker list while cleaning up
// from a savegame error so we can't assume that any thinker that gets here is an error.
curr->Destroy();
}
curr->ObjectFlags |= OF_Cleanup;
delete curr;
finalized++;
}
}
if (finalize_count != NULL)
{
*finalize_count = finalized;
}
return p;
}
//==========================================================================
//
// Mark
//
// Mark a single object gray.
//
//==========================================================================
void Mark(DObject **obj)
{
DObject *lobj = *obj;
//assert(lobj == nullptr || !(lobj->ObjectFlags & OF_Released));
if (lobj != nullptr && !(lobj->ObjectFlags & OF_Released))
{
if (lobj->ObjectFlags & OF_EuthanizeMe)
{
*obj = (DObject *)NULL;
}
else if (lobj->IsWhite())
{
lobj->White2Gray();
lobj->GCNext = Gray;
Gray = lobj;
}
}
}
//==========================================================================
//
// MarkArray
//
// Mark an array of objects gray.
//
//==========================================================================
void MarkArray(DObject **obj, size_t count)
{
for (size_t i = 0; i < count; ++i)
{
Mark(obj[i]);
}
}
//==========================================================================
//
// CalcStepSize
//
// Decide how big a step should be based, depending on how long it took to
// allocate up to the threshold from the amount left after the previous
// collection.
//
//==========================================================================
static size_t CalcStepSize()
{
int time_passed = CheckTime - LastCollectTime;
auto alloc = min(LastCollectAlloc, Estimate);
size_t bytes_gained = AllocBytes > alloc ? AllocBytes - alloc : 0;
return (StepMul > 0 && time_passed > 0)
? std::max<size_t>(GCSTEPSIZE, bytes_gained / time_passed * StepMul / 100)
: std::numeric_limits<size_t>::max() / 2; // no limit
}
//==========================================================================
//
// MarkRoot
//
// Mark the root set of objects.
//
//==========================================================================
TArray<GCMarkerFunc> markers;
void AddMarkerFunc(GCMarkerFunc func)
{
if (markers.Find(func) == markers.Size())
markers.Push(func);
}
static void MarkRoot()
{
Gray = NULL;
for (auto func : markers) func();
// Mark soft roots.
if (SoftRoots != NULL)
{
DObject **probe = &SoftRoots->ObjNext;
while (*probe != NULL)
{
DObject *soft = *probe;
probe = &soft->ObjNext;
if ((soft->ObjectFlags & (OF_Rooted | OF_EuthanizeMe)) == OF_Rooted)
{
Mark(soft);
}
}
}
// Time to propagate the marks.
State = GCS_Propagate;
StepCount = 0;
}
//==========================================================================
//
// Atomic
//
// If there were any propagations that needed to be done atomicly, they
// would go here. It also sets things up for the sweep state.
//
//==========================================================================
static void Atomic()
{
// Flip current white
CurrentWhite = OtherWhite();
SweepPos = &Root;
State = GCS_Sweep;
Estimate = AllocBytes;
// Now that we are about to start a sweep, establish a baseline minimum
// step size for how much memory we want to sweep each CheckGC().
MinStepSize = CalcStepSize();
}
//==========================================================================
//
// SingleStep
//
// Performs one step of the collector.
//
//==========================================================================
static size_t SingleStep()
{
switch (State)
{
case GCS_Pause:
MarkRoot(); // Start a new collection
return 0;
case GCS_Propagate:
if (Gray != NULL)
{
return PropagateMark();
}
else
{ // no more gray objects
Atomic(); // finish mark phase
return 0;
}
case GCS_Sweep: {
size_t old = AllocBytes;
size_t finalize_count;
SweepPos = SweepList(SweepPos, GCSWEEPMAX, &finalize_count);
if (*SweepPos == NULL)
{ // Nothing more to sweep?
State = GCS_Finalize;
}
//assert(old >= AllocBytes);
Estimate -= max<size_t>(0, old - AllocBytes);
return (GCSWEEPMAX - finalize_count) * GCSWEEPCOST + finalize_count * GCFINALIZECOST;
}
case GCS_Finalize:
State = GCS_Pause; // end collection
LastCollectAlloc = AllocBytes;
LastCollectTime = CheckTime;
return 0;
default:
assert(0);
return 0;
}
}
//==========================================================================
//
// Step
//
// Performs enough single steps to cover GCSTEPSIZE * StepMul% bytes of
// memory.
//
//==========================================================================
void Step()
{
// We recalculate a step size in case the rate of allocation went up
// since we started sweeping because we don't want to fall behind.
// However, we also don't want to go slower than what was decided upon
// when the sweep began if the rate of allocation has slowed.
size_t lim = max(CalcStepSize(), MinStepSize);
do
{
size_t done = SingleStep();
if (done < lim)
{
lim -= done;
}
else
{
lim = 0;
}
} while (lim && State != GCS_Pause);
if (State != GCS_Pause)
{
Threshold = AllocBytes;
}
else
{
assert(AllocBytes >= Estimate);
SetThreshold();
}
StepCount++;
}
//==========================================================================
//
// FullGC
//
// Collects everything in one fell swoop.
//
//==========================================================================
void FullGC()
{
if (State <= GCS_Propagate)
{
// Reset sweep mark to sweep all elements (returning them to white)
SweepPos = &Root;
// Reset other collector lists
Gray = NULL;
State = GCS_Sweep;
}
// Finish any pending sweep phase
while (State != GCS_Finalize)
{
SingleStep();
}
MarkRoot();
while (State != GCS_Pause)
{
SingleStep();
}
SetThreshold();
}
//==========================================================================
//
// Barrier
//
// Implements a write barrier to maintain the invariant that a black node
// never points to a white node by making the node pointed at gray.
//
//==========================================================================
void Barrier(DObject *pointing, DObject *pointed)
{
assert(pointing == NULL || (pointing->IsBlack() && !pointing->IsDead()));
assert(pointed->IsWhite() && !pointed->IsDead());
assert(State != GCS_Finalize && State != GCS_Pause);
assert(!(pointed->ObjectFlags & OF_Released)); // if a released object gets here, something must be wrong.
if (pointed->ObjectFlags & OF_Released) return; // don't do anything with non-GC'd objects.
// The invariant only needs to be maintained in the propagate state.
if (State == GCS_Propagate)
{
pointed->White2Gray();
pointed->GCNext = Gray;
Gray = pointed;
}
// In other states, we can mark the pointing object white so this
// barrier won't be triggered again, saving a few cycles in the future.
else if (pointing != NULL)
{
pointing->MakeWhite();
}
}
void DelSoftRootHead()
{
if (SoftRoots != NULL)
{
// Don't let the destructor print a warning message
SoftRoots->ObjectFlags |= OF_YesReallyDelete;
delete SoftRoots;
}
SoftRoots = NULL;
}
//==========================================================================
//
// AddSoftRoot
//
// Marks an object as a soft root. A soft root behaves exactly like a root
// in MarkRoot, except it can be added at run-time.
//
//==========================================================================
void AddSoftRoot(DObject *obj)
{
DObject **probe;
// Are there any soft roots yet?
if (SoftRoots == NULL)
{
// Create a new object to root the soft roots off of, and stick
// it at the end of the object list, so we know that anything
// before it is not a soft root.
SoftRoots = Create<DObject>();
SoftRoots->ObjectFlags |= OF_Fixed;
probe = &Root;
while (*probe != NULL)
{
probe = &(*probe)->ObjNext;
}
Root = SoftRoots->ObjNext;
SoftRoots->ObjNext = NULL;
*probe = SoftRoots;
}
// Mark this object as rooted and move it after the SoftRoots marker.
probe = &Root;
while (*probe != NULL && *probe != obj)
{
probe = &(*probe)->ObjNext;
}
*probe = (*probe)->ObjNext;
obj->ObjNext = SoftRoots->ObjNext;
SoftRoots->ObjNext = obj;
obj->ObjectFlags |= OF_Rooted;
WriteBarrier(obj);
}
//==========================================================================
//
// DelSoftRoot
//
// Unroots an object so that it must be reachable or it will get collected.
//
//==========================================================================
void DelSoftRoot(DObject *obj)
{
DObject **probe;
if (!(obj->ObjectFlags & OF_Rooted))
{ // Not rooted, so nothing to do.
return;
}
obj->ObjectFlags &= ~OF_Rooted;
// Move object out of the soft roots part of the list.
probe = &SoftRoots;
while (*probe != NULL && *probe != obj)
{
probe = &(*probe)->ObjNext;
}
if (*probe == obj)
{
*probe = obj->ObjNext;
obj->ObjNext = Root;
Root = obj;
}
}
}
//==========================================================================
//
// STAT gc
//
// Provides information about the current garbage collector state.
//
//==========================================================================
ADD_STAT(gc)
{
static const char *StateStrings[] = {
" Pause ",
"Propagate",
" Sweep ",
"Finalize " };
FString out;
out.Format("[%s] Alloc:%6zuK Thresh:%6zuK Est:%6zuK Steps: %d %zuK",
StateStrings[GC::State],
(GC::AllocBytes + 1023) >> 10,
(GC::Threshold + 1023) >> 10,
(GC::Estimate + 1023) >> 10,
GC::StepCount,
(GC::MinStepSize + 1023) >> 10);
return out;
}
//==========================================================================
//
// CCMD gc
//
// Controls various aspects of the collector.
//
//==========================================================================
CCMD(gc)
{
if (argv.argc() == 1)
{
Printf ("Usage: gc stop|now|full|count|pause [size]|stepmul [size]\n");
return;
}
if (stricmp(argv[1], "stop") == 0)
{
GC::Threshold = ~(size_t)0 - 2;
}
else if (stricmp(argv[1], "now") == 0)
{
GC::Threshold = GC::AllocBytes;
}
else if (stricmp(argv[1], "full") == 0)
{
GC::FullGC();
}
else if (stricmp(argv[1], "count") == 0)
{
int cnt = 0;
for (DObject *obj = GC::Root; obj; obj = obj->ObjNext, cnt++);
Printf("%d active objects counted\n", cnt);
}
else if (stricmp(argv[1], "pause") == 0)
{
if (argv.argc() == 2)
{
Printf ("Current GC pause is %d\n", GC::Pause);
}
else
{
GC::Pause = max(1,atoi(argv[2]));
}
}
else if (stricmp(argv[1], "stepmul") == 0)
{
if (argv.argc() == 2)
{
Printf ("Current GC stepmul is %d\n", GC::StepMul);
}
else
{
GC::StepMul = max(100, atoi(argv[2]));
}
}
}