Skip to content

Instantly share code, notes, and snippets.

@equalent
Created November 6, 2022 14:20
Show Gist options
  • Save equalent/a55c7648d84eedc1e9467bc6b756f9a6 to your computer and use it in GitHub Desktop.
Save equalent/a55c7648d84eedc1e9467bc6b756f9a6 to your computer and use it in GitHub Desktop.
MakeID.h
#ifndef MAKEID_H
#define MAKEID_H
/*
Author:
Emil Persson, A.K.A. Humus.
http://www.humus.name
Version history:
1.0 - Initial release.
1.01 - Code review fixes. Code reviewed by Denis A. Gladkiy.
1.02 - Fixed an off-by-one error in DestroyRange() found by Markus Billeter
License:
Public Domain
This file is released in the hopes that it will be useful. Use in whatever way you like, but no guarantees that it
actually works or fits any particular purpose. It has been unit-tested and benchmarked though, and seems to do
what it was designed to do, and seems pretty quick at it too.
Notes:
There are many applications where it is desired to generate unique IDs at runtime for various resources, such that they can be
distinguished, sorted or otherwise processed in an efficient manner. It can in some cases replace hashes, handles and pointers.
In cases where resource pointers are used as IDs, it offers a unique ID that requires far fewer bits, especially for 64bit apps.
The design goal of this implementation was to return the most compact IDs as possible, limiting to a specific range if necessary.
The properties of this system are as follows:
- Creating a new ID returns the smallest possible unused ID.
- Creating a new range of IDs returns the smallest possible continuous range of the specified size.
- Created IDs remain valid until destroyed.
- Destroying an ID returns it to the pool and may be returned by subsequent allocations.
- The system is NOT thread-safe.
Performance properties:
- Creating an ID is O(1) and generally super-cheap.
- Destroying an ID is also cheap, but O(log(n)), where n is the current number of distinct available ranges.
- The system merges available ranges when IDs are destroyed, keeping said n generally very small in practice.
- After warmup, no further memory allocations should be necessary, or be very rare.
- The system uses very little memory.
- It is possible to construct a pathological case where fragmentation would cause n to become large. This can be done by
first allocating a very large range of IDs, then deleting every other ID, causing a new range to be allocated for every
free ID, or as many ranges as there are free IDs. I believe nothing close to this situation happens in practical applications.
In tests, millions of random scattered creations and deletions only resulted in a relatively short list in the worst case.
This is because freed IDs are quickly reused and ranges eagerly merged.
Where would this system be useful? It was originally thought up as a replacement for resource pointers as part of sort-ids
in rendering. Using for instance a 64-bit sort-id packing various flags and states, putting a pointer in there takes an
awful lot of bits, especially considering the actual possible resources range in the thousands at most. This got far worse
of course with the switch to 64bit as pointers are now twice as large and essentially eats all bits except bottom few for
alignment.
Another application would be for managing a shared pool of resources. IDs could be handed out as handles and used to access
the actual resource from an array. By always returning the lowest possible ID or range of IDs we get very good cache behavior
since all active resources will grouped together in the bottom part of the array. Using IDs instead of pointers for handles
also allows easy resizing of the allocated memory since IDs can remain the same even if the underlying storage changed.
*/
#include <cstdio> // For printf(). Remove if you don't need the PrintRanges() function (mostly for debugging anyway).
#include <cstdint> // uint32_t
#include <cstdlib>
#include <cstring>
class MakeID
{
private:
// Change to uint16_t here for a more compact implementation if 16bit or less IDs work for you.
typedef uint32_t uint;
struct Range
{
uint m_First;
uint m_Last;
};
Range *m_Ranges; // Sorted array of ranges of free IDs
uint m_Count; // Number of ranges in list
uint m_Capacity; // Total capacity of range list
MakeID & operator=(const MakeID &);
MakeID(const MakeID &);
public:
explicit MakeID(const uint max_id)
{
// Start with a single range, from 0 to max allowed ID (specified)
m_Ranges = static_cast<Range*>(::malloc(sizeof(Range)));
m_Ranges[0].m_First = 0;
m_Ranges[0].m_Last = max_id;
m_Count = 1;
m_Capacity = 1;
}
~MakeID()
{
::free(m_Ranges);
}
bool CreateID(uint &id)
{
if (m_Ranges[0].m_First <= m_Ranges[0].m_Last)
{
id = m_Ranges[0].m_First;
// If current range is full and there is another one, that will become the new current range
if (m_Ranges[0].m_First == m_Ranges[0].m_Last && m_Count > 1)
{
DestroyRange(0);
}
else
{
++m_Ranges[0].m_First;
}
return true;
}
// No availble ID left
return false;
}
bool CreateRangeID(uint &id, const uint count)
{
uint i = 0;
do
{
const uint range_count = 1 + m_Ranges[i].m_Last - m_Ranges[i].m_First;
if (count <= range_count)
{
id = m_Ranges[i].m_First;
// If current range is full and there is another one, that will become the new current range
if (count == range_count && i + 1 < m_Count)
{
DestroyRange(i);
}
else
{
m_Ranges[i].m_First += count;
}
return true;
}
++i;
} while (i < m_Count);
// No range of free IDs was large enough to create the requested continuous ID sequence
return false;
}
bool DestroyID(const uint id)
{
return DestroyRangeID(id, 1);
}
bool DestroyRangeID(const uint id, const uint count)
{
const uint end_id = id + count;
// Binary search of the range list
uint i0 = 0;
uint i1 = m_Count - 1;
for (;;)
{
const uint i = (i0 + i1) / 2;
if (id < m_Ranges[i].m_First)
{
// Before current range, check if neighboring
if (end_id >= m_Ranges[i].m_First)
{
if (end_id != m_Ranges[i].m_First)
return false; // Overlaps a range of free IDs, thus (at least partially) invalid IDs
// Neighbor id, check if neighboring previous range too
if (i > i0 && id - 1 == m_Ranges[i - 1].m_Last)
{
// Merge with previous range
m_Ranges[i - 1].m_Last = m_Ranges[i].m_Last;
DestroyRange(i);
}
else
{
// Just grow range
m_Ranges[i].m_First = id;
}
return true;
}
else
{
// Non-neighbor id
if (i != i0)
{
// Cull upper half of list
i1 = i - 1;
}
else
{
// Found our position in the list, insert the deleted range here
InsertRange(i);
m_Ranges[i].m_First = id;
m_Ranges[i].m_Last = end_id - 1;
return true;
}
}
}
else if (id > m_Ranges[i].m_Last)
{
// After current range, check if neighboring
if (id - 1 == m_Ranges[i].m_Last)
{
// Neighbor id, check if neighboring next range too
if (i < i1 && end_id == m_Ranges[i + 1].m_First)
{
// Merge with next range
m_Ranges[i].m_Last = m_Ranges[i + 1].m_Last;
DestroyRange(i + 1);
}
else
{
// Just grow range
m_Ranges[i].m_Last += count;
}
return true;
}
else
{
// Non-neighbor id
if (i != i1)
{
// Cull bottom half of list
i0 = i + 1;
}
else
{
// Found our position in the list, insert the deleted range here
InsertRange(i + 1);
m_Ranges[i + 1].m_First = id;
m_Ranges[i + 1].m_Last = end_id - 1;
return true;
}
}
}
else
{
// Inside a free block, not a valid ID
return false;
}
}
}
bool IsID(const uint id) const
{
// Binary search of the range list
uint i0 = 0;
uint i1 = m_Count - 1;
for (;;)
{
const uint i = (i0 + i1) / 2;
if (id < m_Ranges[i].m_First)
{
if (i == i0)
return true;
// Cull upper half of list
i1 = i - 1;
}
else if (id > m_Ranges[i].m_Last)
{
if (i == i1)
return true;
// Cull bottom half of list
i0 = i + 1;
}
else
{
// Inside a free block, not a valid ID
return false;
}
}
}
uint GetAvailableIDs() const
{
uint count = m_Count;
uint i = 0;
do
{
count += m_Ranges[i].m_Last - m_Ranges[i].m_First;
++i;
} while (i < m_Count);
return count;
}
uint GetLargestContinuousRange() const
{
uint max_count = 0;
uint i = 0;
do
{
uint count = m_Ranges[i].m_Last - m_Ranges[i].m_First + 1;
if (count > max_count)
max_count = count;
++i;
} while (i < m_Count);
return max_count;
}
void PrintRanges() const
{
uint i = 0;
for (;;)
{
if (m_Ranges[i].m_First < m_Ranges[i].m_Last)
printf("%u-%u", m_Ranges[i].m_First, m_Ranges[i].m_Last);
else if (m_Ranges[i].m_First == m_Ranges[i].m_Last)
printf("%u", m_Ranges[i].m_First);
else
printf("-");
++i;
if (i >= m_Count)
{
printf("\n");
return;
}
printf(", ");
}
}
private:
void InsertRange(const uint index)
{
if (m_Count >= m_Capacity)
{
m_Capacity += m_Capacity;
m_Ranges = (Range *) realloc(m_Ranges, m_Capacity * sizeof(Range));
}
::memmove(m_Ranges + index + 1, m_Ranges + index, (m_Count - index) * sizeof(Range));
++m_Count;
}
void DestroyRange(const uint index)
{
--m_Count;
::memmove(m_Ranges + index, m_Ranges + index + 1, (m_Count - index) * sizeof(Range));
}
};
#endif // MAKEID_H
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment