1+ #ifndef MAKEID_H
2+ #define MAKEID_H
3+
4+ /*
5+
6+ Author:
7+ Emil Persson, A.K.A. Humus.
8+ http://www.humus.name
9+
10+ Version history:
11+ 1.0 - Initial release.
12+ 1.01 - Code review fixes. Code reviewed by Denis A. Gladkiy.
13+ 1.02 - Fixed an off-by-one error in DestroyRange() found by Markus Billeter
14+
15+ License:
16+ Public Domain
17+
18+ This file is released in the hopes that it will be useful. Use in whatever way you like, but no guarantees that it
19+ actually works or fits any particular purpose. It has been unit-tested and benchmarked though, and seems to do
20+ what it was designed to do, and seems pretty quick at it too.
21+
22+ Notes:
23+ There are many applications where it is desired to generate unique IDs at runtime for various resources, such that they can be
24+ distinguished, sorted or otherwise processed in an efficient manner. It can in some cases replace hashes, handles and pointers.
25+ In cases where resource pointers are used as IDs, it offers a unique ID that requires far fewer bits, especially for 64bit apps.
26+ The design goal of this implementation was to return the most compact IDs as possible, limiting to a specific range if necessary.
27+
28+ The properties of this system are as follows:
29+ - Creating a new ID returns the smallest possible unused ID.
30+ - Creating a new range of IDs returns the smallest possible continuous range of the specified size.
31+ - Created IDs remain valid until destroyed.
32+ - Destroying an ID returns it to the pool and may be returned by subsequent allocations.
33+ - The system is NOT thread-safe.
34+
35+ Performance properties:
36+ - Creating an ID is O(1) and generally super-cheap.
37+ - Destroying an ID is also cheap, but O(log(n)), where n is the current number of distinct available ranges.
38+ - The system merges available ranges when IDs are destroyed, keeping said n generally very small in practice.
39+ - After warmup, no further memory allocations should be necessary, or be very rare.
40+ - The system uses very little memory.
41+ - It is possible to construct a pathological case where fragmentation would cause n to become large. This can be done by
42+ first allocating a very large range of IDs, then deleting every other ID, causing a new range to be allocated for every
43+ free ID, or as many ranges as there are free IDs. I believe nothing close to this situation happens in practical applications.
44+ In tests, millions of random scattered creations and deletions only resulted in a relatively short list in the worst case.
45+ This is because freed IDs are quickly reused and ranges eagerly merged.
46+
47+ Where would this system be useful? It was originally thought up as a replacement for resource pointers as part of sort-ids
48+ in rendering. Using for instance a 64-bit sort-id packing various flags and states, putting a pointer in there takes an
49+ awful lot of bits, especially considering the actual possible resources range in the thousands at most. This got far worse
50+ of course with the switch to 64bit as pointers are now twice as large and essentially eats all bits except bottom few for
51+ alignment.
52+ Another application would be for managing a shared pool of resources. IDs could be handed out as handles and used to access
53+ the actual resource from an array. By always returning the lowest possible ID or range of IDs we get very good cache behavior
54+ since all active resources will grouped together in the bottom part of the array. Using IDs instead of pointers for handles
55+ also allows easy resizing of the allocated memory since IDs can remain the same even if the underlying storage changed.
56+
57+ */
58+
59+
60+ #include < cstdio> // For printf(). Remove if you don't need the PrintRanges() function (mostly for debugging anyway).
61+ #include < cstdint> // uint32_t
62+ #include < cstdlib>
63+ #include < cstring>
64+
65+ class MakeID
66+ {
67+ private:
68+ // Change to uint16_t here for a more compact implementation if 16bit or less IDs work for you.
69+ typedef uint32_t uint;
70+
71+ struct Range
72+ {
73+ uint m_First;
74+ uint m_Last;
75+ };
76+
77+ Range *m_Ranges; // Sorted array of ranges of free IDs
78+ uint m_Count; // Number of ranges in list
79+ uint m_Capacity; // Total capacity of range list
80+
81+ MakeID & operator =(const MakeID &);
82+ MakeID (const MakeID &);
83+
84+ public:
85+ explicit MakeID (const uint max_id)
86+ {
87+ // Start with a single range, from 0 to max allowed ID (specified)
88+ m_Ranges = static_cast <Range*>(::malloc (sizeof (Range)));
89+ m_Ranges[0 ].m_First = 0 ;
90+ m_Ranges[0 ].m_Last = max_id;
91+ m_Count = 1 ;
92+ m_Capacity = 1 ;
93+ }
94+
95+ ~MakeID ()
96+ {
97+ ::free (m_Ranges);
98+ }
99+
100+
101+ bool CreateID (uint &id)
102+ {
103+ if (m_Ranges[0 ].m_First <= m_Ranges[0 ].m_Last )
104+ {
105+ id = m_Ranges[0 ].m_First ;
106+
107+ // If current range is full and there is another one, that will become the new current range
108+ if (m_Ranges[0 ].m_First == m_Ranges[0 ].m_Last && m_Count > 1 )
109+ {
110+ DestroyRange (0 );
111+ }
112+ else
113+ {
114+ ++m_Ranges[0 ].m_First ;
115+ }
116+ return true ;
117+ }
118+
119+ // No availble ID left
120+ return false ;
121+ }
122+
123+ bool CreateRangeID (uint &id, const uint count)
124+ {
125+ uint i = 0 ;
126+ do
127+ {
128+ const uint range_count = 1 + m_Ranges[i].m_Last - m_Ranges[i].m_First ;
129+ if (count <= range_count)
130+ {
131+ id = m_Ranges[i].m_First ;
132+
133+ // If current range is full and there is another one, that will become the new current range
134+ if (count == range_count && i + 1 < m_Count)
135+ {
136+ DestroyRange (i);
137+ }
138+ else
139+ {
140+ m_Ranges[i].m_First += count;
141+ }
142+ return true ;
143+ }
144+ ++i;
145+ } while (i < m_Count);
146+
147+ // No range of free IDs was large enough to create the requested continuous ID sequence
148+ return false ;
149+ }
150+
151+ bool DestroyID (const uint id)
152+ {
153+ return DestroyRangeID (id, 1 );
154+ }
155+
156+ bool DestroyRangeID (const uint id, const uint count)
157+ {
158+ const uint end_id = id + count;
159+
160+ // Binary search of the range list
161+ uint i0 = 0 ;
162+ uint i1 = m_Count - 1 ;
163+
164+ for (;;)
165+ {
166+ const uint i = (i0 + i1) / 2 ;
167+
168+ if (id < m_Ranges[i].m_First )
169+ {
170+ // Before current range, check if neighboring
171+ if (end_id >= m_Ranges[i].m_First )
172+ {
173+ if (end_id != m_Ranges[i].m_First )
174+ return false ; // Overlaps a range of free IDs, thus (at least partially) invalid IDs
175+
176+ // Neighbor id, check if neighboring previous range too
177+ if (i > i0 && id - 1 == m_Ranges[i - 1 ].m_Last )
178+ {
179+ // Merge with previous range
180+ m_Ranges[i - 1 ].m_Last = m_Ranges[i].m_Last ;
181+ DestroyRange (i);
182+ }
183+ else
184+ {
185+ // Just grow range
186+ m_Ranges[i].m_First = id;
187+ }
188+ return true ;
189+ }
190+ else
191+ {
192+ // Non-neighbor id
193+ if (i != i0)
194+ {
195+ // Cull upper half of list
196+ i1 = i - 1 ;
197+ }
198+ else
199+ {
200+ // Found our position in the list, insert the deleted range here
201+ InsertRange (i);
202+ m_Ranges[i].m_First = id;
203+ m_Ranges[i].m_Last = end_id - 1 ;
204+ return true ;
205+ }
206+ }
207+ }
208+ else if (id > m_Ranges[i].m_Last )
209+ {
210+ // After current range, check if neighboring
211+ if (id - 1 == m_Ranges[i].m_Last )
212+ {
213+ // Neighbor id, check if neighboring next range too
214+ if (i < i1 && end_id == m_Ranges[i + 1 ].m_First )
215+ {
216+ // Merge with next range
217+ m_Ranges[i].m_Last = m_Ranges[i + 1 ].m_Last ;
218+ DestroyRange (i + 1 );
219+ }
220+ else
221+ {
222+ // Just grow range
223+ m_Ranges[i].m_Last += count;
224+ }
225+ return true ;
226+ }
227+ else
228+ {
229+ // Non-neighbor id
230+ if (i != i1)
231+ {
232+ // Cull bottom half of list
233+ i0 = i + 1 ;
234+ }
235+ else
236+ {
237+ // Found our position in the list, insert the deleted range here
238+ InsertRange (i + 1 );
239+ m_Ranges[i + 1 ].m_First = id;
240+ m_Ranges[i + 1 ].m_Last = end_id - 1 ;
241+ return true ;
242+ }
243+ }
244+ }
245+ else
246+ {
247+ // Inside a free block, not a valid ID
248+ return false ;
249+ }
250+
251+ }
252+ }
253+
254+ bool IsID (const uint id) const
255+ {
256+ // Binary search of the range list
257+ uint i0 = 0 ;
258+ uint i1 = m_Count - 1 ;
259+
260+ for (;;)
261+ {
262+ const uint i = (i0 + i1) / 2 ;
263+
264+ if (id < m_Ranges[i].m_First )
265+ {
266+ if (i == i0)
267+ return true ;
268+
269+ // Cull upper half of list
270+ i1 = i - 1 ;
271+ }
272+ else if (id > m_Ranges[i].m_Last )
273+ {
274+ if (i == i1)
275+ return true ;
276+
277+ // Cull bottom half of list
278+ i0 = i + 1 ;
279+ }
280+ else
281+ {
282+ // Inside a free block, not a valid ID
283+ return false ;
284+ }
285+
286+ }
287+ }
288+
289+ uint GetAvailableIDs () const
290+ {
291+ uint count = m_Count;
292+ uint i = 0 ;
293+
294+ do
295+ {
296+ count += m_Ranges[i].m_Last - m_Ranges[i].m_First ;
297+ ++i;
298+ } while (i < m_Count);
299+
300+ return count;
301+ }
302+
303+ uint GetLargestContinuousRange () const
304+ {
305+ uint max_count = 0 ;
306+ uint i = 0 ;
307+
308+ do
309+ {
310+ uint count = m_Ranges[i].m_Last - m_Ranges[i].m_First + 1 ;
311+ if (count > max_count)
312+ max_count = count;
313+
314+ ++i;
315+ } while (i < m_Count);
316+
317+ return max_count;
318+ }
319+
320+ void PrintRanges () const
321+ {
322+ uint i = 0 ;
323+ for (;;)
324+ {
325+ if (m_Ranges[i].m_First < m_Ranges[i].m_Last )
326+ printf (" %u-%u" , m_Ranges[i].m_First , m_Ranges[i].m_Last );
327+ else if (m_Ranges[i].m_First == m_Ranges[i].m_Last )
328+ printf (" %u" , m_Ranges[i].m_First );
329+ else
330+ printf (" -" );
331+
332+ ++i;
333+ if (i >= m_Count)
334+ {
335+ printf (" \n " );
336+ return ;
337+ }
338+
339+ printf (" , " );
340+ }
341+ }
342+
343+
344+ private:
345+
346+ void InsertRange (const uint index)
347+ {
348+ if (m_Count >= m_Capacity)
349+ {
350+ m_Capacity += m_Capacity;
351+ m_Ranges = (Range *) realloc (m_Ranges, m_Capacity * sizeof (Range));
352+ }
353+
354+ ::memmove (m_Ranges + index + 1 , m_Ranges + index, (m_Count - index) * sizeof(Range));
355+ ++m_Count;
356+ }
357+
358+ void DestroyRange (const uint index)
359+ {
360+ --m_Count;
361+ ::memmove (m_Ranges + index, m_Ranges + index + 1 , (m_Count - index) * sizeof(Range));
362+ }
363+ };
364+
365+ #endif // MAKEID_H
0 commit comments