|
| 1 | +<?xml version='1.0' encoding='utf-8' standalone='no'?> |
| 2 | +<!DOCTYPE issue SYSTEM "lwg-issue.dtd"> |
| 3 | + |
| 4 | +<issue num="4320" status="New"> |
| 5 | +<title>`hive` operations involving insertion/erasure should have <tt>𝒪(log n)</tt> time complexity</title> |
| 6 | +<section> |
| 7 | +<sref ref="[hive]"/> |
| 8 | +</section> |
| 9 | +<submitter>Matt Bentley</submitter> |
| 10 | +<date>19 Aug 2025</date> |
| 11 | +<priority>99</priority> |
| 12 | + |
| 13 | +<discussion> |
| 14 | +<p> |
| 15 | +Under <sref ref="[hive.modifiers]"/> p4 complexity is stated as "Constant. |
| 16 | +Exactly one object of type `T` is constructed." |
| 17 | +<p/> |
| 18 | +However the approach to implementation necessary to support 8/16-bit |
| 19 | +types without artificially widening the type storage, as described under |
| 20 | +<a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p0447r28.html#non_reference_implementations_info"> |
| 21 | +"Additional info for supporting small types" in P0447</a> basically specifies |
| 22 | +time complexity which is `Log(n)` in the capacity of the element block selected |
| 23 | +to insert into (when erased element memory locations are available for reuse) but |
| 24 | +imposes a small maximum block capacity cap. This time complexity only occurs |
| 25 | +during the operation to find an erased element memory location within a block |
| 26 | +which is known to have one. |
| 27 | +<p/> |
| 28 | +This is both the simplest and fastest solution to supporting small types |
| 29 | +in hive without artificial widening that I have come across. |
| 30 | +<p/> |
| 31 | +Further, I have discovered that this approach can be extended to larger |
| 32 | +block capacities via |
| 33 | +<a href="https://plflib.org/matt_bentley_-_bitset_stacking.pdf">"bitset stacking"</a> |
| 34 | +while retaining <tt>𝒪(log n)</tt> intra-block lookup time complexity, |
| 35 | +regardless of block capacity. Overall this approach would be useful for embedded |
| 36 | +and other memory-scarse platforms as it reduces the 16-bit-per-element cost of the |
| 37 | +reference implementation down to a 1-bit-per-element cost. For 64-bit and larger types, |
| 38 | +there are other ways to obtain this reduction without |
| 39 | +losing <tt>𝒪(1)</tt> lookup but it is unclear whether those methods would |
| 40 | +in fact be faster. |
| 41 | +<p/> |
| 42 | +Regardless, it is necessary for small types. In all cases <tt>N</tt> is capped by |
| 43 | +the maximum block capacity defined in the implementation. |
| 44 | +<p/> |
| 45 | +There is ambiguity as to whether this should result in a change to <tt>hive::insert/emplace</tt> |
| 46 | +time complexity when discussed on the reflector, as it is unrelated to element numbers (unless |
| 47 | +all elements fit within one block), but it is related to block capacities, which are defined |
| 48 | +as part of the `hive` technical specification. |
| 49 | +<p/> |
| 50 | +The exact mechanism by which erased element locations are recorded (for later reuse |
| 51 | +<sref ref="[hive.overview]"/> p3) in `hive` is not specified in the technical |
| 52 | +specification. The issue is therefore in some ways similar to `deque` `insert/emplace` |
| 53 | +time complexity, where a `push_back` may in fact result in a <tt>𝒪(n)</tt> |
| 54 | +operation in the number of blocks — but because we do not specify the storage |
| 55 | +mechanism in `deque`, we ignore the fact that deques are typically constructed as |
| 56 | +vectors of pointers to blocks, and therefore any insert may trigger vector expansion. |
| 57 | +This was also the reasoning discussed for `hive` during LWG talks, since it can also |
| 58 | +be implemented as a vector of pointers to blocks. |
| 59 | +<p/> |
| 60 | +In addition, if we were to support approaches involving bitset stacking, this would |
| 61 | +also affect functions which can erase elements, since the number of modifications to |
| 62 | +the bitset-stack of a given block would increase by 1 with every 64x increase (assuming |
| 63 | +64-bit words) in block capacity. |
| 64 | +</p> |
| 65 | +<blockquote class="note"> |
| 66 | +<p> |
| 67 | +I do not personally believe any wording change are necessary here, based on |
| 68 | +precedents set by the existing standard and what has been conveyed to me |
| 69 | +in the past regarding this topic (<tt>𝒪</tt> complexity within blocks). |
| 70 | +However reflector discussion on the subject has not been entirely conclusive |
| 71 | +since the relevance of some time complexity involves a degree of subjectivity. |
| 72 | +This issue has therefore been submitted in order for a definitive conclusion |
| 73 | +to be reached on the issue. |
| 74 | +</p> |
| 75 | +</blockquote> |
| 76 | +<p> |
| 77 | +<b>Changes necessary:</b> |
| 78 | +<p/> |
| 79 | +Making this change would affect any functions which may reuse existing |
| 80 | +erased-element memory locations. This includes: |
| 81 | +<p/> |
| 82 | +<sref ref="[hive.modifiers]"/>:<br/> |
| 83 | +(`emplace`) p4<br/> |
| 84 | +(range `insert`) p9<br/> |
| 85 | +(fill `insert`) p13 |
| 86 | +<p/> |
| 87 | +<sref ref="[hive.capacity]"/>:<br/> |
| 88 | +(`shrink_to_fit`) p10<br/> |
| 89 | +(`reshape`) p24 |
| 90 | +<p/> |
| 91 | +(both of the above functions may potentially relocate existing elements |
| 92 | +from one block to erased element locations in another) |
| 93 | +<p/> |
| 94 | +<sref ref="[hive.cons]"/>:<br/> |
| 95 | +(<tt>operator= &</tt>) p27<br/> |
| 96 | +(<tt>operator= &&</tt>) p31 |
| 97 | +<p/> |
| 98 | +(move assignment will switch to moving individual elements where |
| 99 | +allocators are not equal - and in the case of non-trivial/allocating |
| 100 | +types, move-assigning to existing elements in the source may be beneficial) |
| 101 | +<p/> |
| 102 | +In addition, if we were to support bitset-stacking approaches these also |
| 103 | +effect functions which erase individual elements. This includes: |
| 104 | +<p/> |
| 105 | +<sref ref="[hive.modifiers]"/>:<br/> |
| 106 | +(`erase`) p16 |
| 107 | +<p/> |
| 108 | +<sref ref="[hive.operations]"/>:<br/> |
| 109 | +(`unique`) p11 |
| 110 | +</p> |
| 111 | +</discussion> |
| 112 | + |
| 113 | +<resolution> |
| 114 | +<p> |
| 115 | +This wording is relative to <paper num="N5014"/>. |
| 116 | +</p> |
| 117 | + |
| 118 | +<blockquote class="note"> |
| 119 | +<p> |
| 120 | +[<i>Drafting note:</i> I am unclear on whether `assign()` and `assign_range()` operations |
| 121 | +would require specification since they also have the capability to reuse existing erased |
| 122 | +element memory spaces, but we do not currently supply time complexity wording for these |
| 123 | +in the standard in general and I'm unsure why that is.] |
| 124 | +</p> |
| 125 | +</blockquote> |
| 126 | + |
| 127 | +<ol> |
| 128 | + |
| 129 | +<li><p>Modify <sref ref="[hive.overview]"/> as indicated:</p> |
| 130 | + |
| 131 | +<blockquote> |
| 132 | +<p> |
| 133 | +-1- A `hive` is a type of sequence container <del>that provides constant-time insertion and erasure |
| 134 | +operations. S</del><ins>where s</ins>torage is automatically managed in multiple memory blocks, referred |
| 135 | +to as element blocks. Insertion position is determined by the container, and <del>insertion</del> |
| 136 | +may re-use the memory locations of erased elements. <ins>Insertions are either constant time |
| 137 | +or logarithmic in the capacity of the element block inserted into.</ins> |
| 138 | +<p/> |
| 139 | +-2- […] |
| 140 | +<p/> |
| 141 | +-3- Erasures use unspecified techniques <del>of constant time complexity</del> to identify the memory |
| 142 | +locations of erased elements, which are subsequently skipped during iteration <ins>in constant time</ins>, |
| 143 | +as opposed to relocating subsequent elements during erasure. <ins>These techniques are either constant time |
| 144 | +or logarithmic in the capacity of the element block erased from.</ins> |
| 145 | +</p> |
| 146 | +</blockquote> |
| 147 | + |
| 148 | +</li> |
| 149 | + |
| 150 | +<li><p>Modify <sref ref="[hive.cons]"/> as indicated:</p> |
| 151 | + |
| 152 | +<blockquote> |
| 153 | +<pre> |
| 154 | +hive& operator=(const hive& x); |
| 155 | +</pre> |
| 156 | +<blockquote> |
| 157 | +<p> |
| 158 | +-25- <i>Preconditions</i>: […] |
| 159 | +<p/> |
| 160 | +-26- <i>Effects</i>: […] |
| 161 | +<p/> |
| 162 | +-27- <i>Complexity</i>: Linear in `size() + x.size()`. <ins>Additionally at worst |
| 163 | +<tt>𝒪(log n)</tt> in the capacity of each element block which an element |
| 164 | +is constructed within.</ins> |
| 165 | +</p> |
| 166 | +</blockquote> |
| 167 | +<pre> |
| 168 | +hive& operator=(hive&& x) |
| 169 | + noexcept(allocator_traits<Allocator>::propagate_on_container_move_assignment::value || |
| 170 | + allocator_traits<Allocator>::is_always_equal::value); |
| 171 | +</pre> |
| 172 | +<blockquote> |
| 173 | +<p> |
| 174 | +-28- <i>Preconditions</i>: […] |
| 175 | +<p/> |
| 176 | +-29- <i>Effects</i>: […] |
| 177 | +<p/> |
| 178 | +-30- <i>Postconditions</i>: […] |
| 179 | +<p/> |
| 180 | +-31- <i>Complexity</i>: Linear in `size()`. If |
| 181 | +</p> |
| 182 | +<blockquote><pre> |
| 183 | +(allocator_traits<Allocator>::propagate_on_container_move_assignment::value || |
| 184 | +get_allocator() == x.get_allocator()) |
| 185 | +</pre></blockquote> |
| 186 | +<p> |
| 187 | +is `false`, also linear in `x.size()` <ins>and additionally at worst |
| 188 | +<tt>𝒪(log n)</tt> in the capacity of each element block which |
| 189 | +an element is constructed within</ins>. |
| 190 | +</p> |
| 191 | +</blockquote> |
| 192 | +</blockquote> |
| 193 | + |
| 194 | +</li> |
| 195 | + |
| 196 | +<li><p>Modify <sref ref="[hive.capacity]"/> as indicated:</p> |
| 197 | + |
| 198 | +<blockquote> |
| 199 | +<pre> |
| 200 | +void shrink_to_fit(); |
| 201 | +</pre> |
| 202 | +<blockquote> |
| 203 | +<p> |
| 204 | +-8- <i>Preconditions</i>: […] |
| 205 | +<p/> |
| 206 | +-9- <i>Effects</i>: […] |
| 207 | +<p/> |
| 208 | +<p/> |
| 209 | +-10- <i>Complexity</i>: If reallocation happens, linear in the size of the sequence |
| 210 | +<ins>and at worst <tt>𝒪(log n)</tt> in the capacity of each element block which |
| 211 | +elements are reallocated into</ins>. |
| 212 | +<p/> |
| 213 | +-11- <i>Remarks</i>: […] |
| 214 | +</p> |
| 215 | +</blockquote> |
| 216 | +<p> |
| 217 | +[…] |
| 218 | +</p> |
| 219 | +<pre> |
| 220 | +void reshape(hive_limits block_limits); |
| 221 | +</pre> |
| 222 | +<blockquote> |
| 223 | +<p> |
| 224 | +-21- <i>Preconditions</i>: […] |
| 225 | +<p/> |
| 226 | +-22- <i>Effects</i>: […] |
| 227 | +<p/> |
| 228 | +-23- <i>Postconditions</i>: […] |
| 229 | +<p/> |
| 230 | +-24- <i>Complexity</i>: Linear in the number of element blocks in `*this`. If reallocation happens, |
| 231 | +also linear in the number of elements reallocated <ins>and at worst <tt>𝒪(log n)</tt> in |
| 232 | +the capacity of each element block which elements are reallocated into</ins>. |
| 233 | +<p/> |
| 234 | +-25- <i>Remarks</i>: […] |
| 235 | +</p> |
| 236 | +</blockquote> |
| 237 | +</blockquote> |
| 238 | + |
| 239 | +</li> |
| 240 | + |
| 241 | +<li><p>Modify <sref ref="[hive.modifiers]"/> as indicated:</p> |
| 242 | + |
| 243 | +<blockquote> |
| 244 | +<pre> |
| 245 | +template<class... Args> iterator emplace(Args&&... args); |
| 246 | +template<class... Args> iterator emplace_hint(const_iterator hint, Args&&... args); |
| 247 | +</pre> |
| 248 | +<blockquote> |
| 249 | +<p> |
| 250 | +-1- <i>Preconditions</i>: […] |
| 251 | +<p/> |
| 252 | +-2- <i>Effects</i>: […] |
| 253 | +<p/> |
| 254 | +-3- <i>Returns</i>: […] |
| 255 | +<p/> |
| 256 | +-4- <i>Complexity</i>: <del>Constant</del><ins>At worst <tt>𝒪(log n)</tt> in the capacity |
| 257 | +of the element block which the element is constructed within</ins>. Exactly one object of type `T` is constructed. |
| 258 | +</p> |
| 259 | +</blockquote> |
| 260 | +<p> |
| 261 | +[…] |
| 262 | +</p> |
| 263 | +<pre> |
| 264 | +void insert(initializer_list<T> rg); |
| 265 | +template<<i>container-compatible-range</i><T> R> |
| 266 | + void insert_range(R&& rg); |
| 267 | +</pre> |
| 268 | +<blockquote> |
| 269 | +<p> |
| 270 | +-7- <i>Preconditions</i>: […] |
| 271 | +<p/> |
| 272 | +-8- <i>Effects</i>: […] |
| 273 | +<p/> |
| 274 | +<p/> |
| 275 | +-9- <i>Complexity</i>: Linear in the number of elements inserted. <ins>Additionally at |
| 276 | +worst <tt>𝒪(log n)</tt> in the capacity of each element block which an element is |
| 277 | +constructed within.</ins> Exactly one object of type `T` is constructed for each element |
| 278 | +inserted. |
| 279 | +<p/> |
| 280 | +-10- <i>Remarks</i>: […] |
| 281 | +</p> |
| 282 | +</blockquote> |
| 283 | +<pre> |
| 284 | +void insert(size_type n, const T& x); |
| 285 | +</pre> |
| 286 | +<blockquote> |
| 287 | +<p> |
| 288 | +-11- <i>Preconditions</i>: […] |
| 289 | +<p/> |
| 290 | +-12- <i>Effects</i>: […] |
| 291 | +<p/> |
| 292 | +<p/> |
| 293 | +-13- <i>Complexity</i>: Linear in `n`. <ins>Additionally at worst <tt>𝒪(log n)</tt> |
| 294 | +in the capacity of each element block which an element is constructed within.</ins>. |
| 295 | +Exactly one object of type `T` is constructed for each element inserted. |
| 296 | +<p/> |
| 297 | +-14- <i>Remarks</i>: […] |
| 298 | +</p> |
| 299 | +</blockquote> |
| 300 | +<p> |
| 301 | +[…] |
| 302 | +</p> |
| 303 | +<pre> |
| 304 | +iterator erase(const_iterator position); |
| 305 | +iterator erase(const_iterator first, const_iterator last); |
| 306 | +</pre> |
| 307 | +<blockquote> |
| 308 | +<p> |
| 309 | +-16- Complexity: Linear in the number of elements erased. Additionally, if any active blocks |
| 310 | +become empty of elements as a result of the function call, at worst linear in the number of |
| 311 | +element blocks. <ins>For any active blocks which do not become empty of elements as a result |
| 312 | +of the function call, at worst <tt>𝒪(log n)</tt> in the capacity of each block |
| 313 | +which has an element erased from it.</ins> |
| 314 | +</p> |
| 315 | +</blockquote> |
| 316 | +</blockquote> |
| 317 | + |
| 318 | +</li> |
| 319 | + |
| 320 | +<li><p>Modify <sref ref="[hive.operations]"/> as indicated:</p> |
| 321 | + |
| 322 | +<blockquote> |
| 323 | +<pre> |
| 324 | +template<class BinaryPredicate = equal_to<T>> |
| 325 | + size_type unique(BinaryPredicate binary_pred = BinaryPredicate()); |
| 326 | +</pre> |
| 327 | +<blockquote> |
| 328 | +<p> |
| 329 | +-7- <i>Preconditions</i>: […] |
| 330 | +<p/> |
| 331 | +-8- <i>Effects</i>: […] |
| 332 | +<p/> |
| 333 | +-9- <i>Returns</i>: […] |
| 334 | +<p/> |
| 335 | +-10- <i>Throws</i>: […] |
| 336 | +<p/> |
| 337 | +-11- <i>Complexity</i>: If `empty()` is `false`, exactly `size() - 1` applications |
| 338 | +of the corresponding predicate, otherwise no applications of the predicate. |
| 339 | +<ins>If erasures occur, also linear in the number of elements erased. |
| 340 | +Additionally, if any active blocks become empty of elements as a result |
| 341 | +of the function call, at worst linear in the number of element blocks. |
| 342 | +For any active blocks which do not become empty of elements as a result |
| 343 | +of the function call, at worst <tt>𝒪(log n)</tt> in the capacity of |
| 344 | +each block which has an element erased from it.</ins> |
| 345 | +<p/> |
| 346 | +-12- <i>Remarks</i>: […] |
| 347 | +</p> |
| 348 | +</blockquote> |
| 349 | +</blockquote> |
| 350 | + |
| 351 | +</li> |
| 352 | + |
| 353 | +</ol> |
| 354 | +</resolution> |
| 355 | + |
| 356 | +</issue> |
0 commit comments