1 module nxt.sso_open_hashset; 2 3 import nxt.container.hybrid_hashmap; 4 5 import std.traits : isInstanceOf; 6 import nxt.nullable_traits : isNullable, isAddress; 7 import nxt.my_mallocator : Mallocator; 8 9 /** Small-Size-Optimized (SSO) `HybridHashSet`. 10 * 11 * TODO: search for `nullify`, `isNull`, `nullValue` and support deleted keys (`isDull`) 12 * 13 * TODO: use opPostMove to update `gc_addRange` and `gc_removeRange` when 14 * implemented. See: https://github.com/dlang/DIPs/pull/109 15 * 16 * See_Also: https://forum.dlang.org/post/pb87rn$2icb$1@digitalmars.com 17 */ 18 struct SSOHybridHashSet(K, alias hasher = hashOf, alias Allocator = Mallocator.instance) 19 if (isNullable!K) 20 { 21 import nxt.qcmeman : gc_addRange, gc_removeRange; 22 23 import core.lifetime : emplace, move, moveEmplace; 24 import std.traits : isDynamicArray; 25 import core.internal.traits : hasElaborateDestructor; 26 import nxt.nullable_traits : defaultNullKeyConstantOf, isNull, nullify; 27 import nxt.bit_traits : isAllZeroBits; 28 29 alias InsertionStatus = Large.InsertionStatus; 30 31 @safe: 32 33 static if (!large.hasAddressLikeKey && 34 (__traits(hasMember, K, `nullValue`) && // if key has a null value 35 __traits(compiles, { enum _ = isAllZeroBits!(K, K.nullValue); }) && // prevent strange error given when `K` is `knet.data.Data` 36 !isAllZeroBits!(K, K.nullValue))) 37 { 38 pragma(msg, "TODO: warning key type ", K, " has non-zero-bit init value, default construction should be disabled or Small._store should be set to init value"); 39 } 40 // TODO: @disable this(); 41 42 static typeof(this) withCapacity()(size_t minimumCapacity) @trusted /* template-lazy */ 43 { 44 typeof(return) result; // TODO: `result = void` for nullify case 45 if (minimumCapacity > Small.maxCapacity) // will be large 46 result.large = Large.withCapacity(minimumCapacity); 47 else // small 48 { 49 static if (Large.hasAddressLikeKey || 50 (__traits(hasMember, K, `nullValue`) && // if key has a null value 51 __traits(compiles, { enum _ = isAllZeroBits!(K, K.nullValue); }) && // prevent strange error given when `K` is `knet.data.Data` 52 isAllZeroBits!(K, K.nullValue))) // check that it's zero bits only 53 { 54 // nothing needed 55 } 56 else // needs explicit null 57 static foreach (immutable index; 0 .. small.maxCapacity) 58 result.small._store[index].nullify(); 59 60 result.small._capacityDummy = 2; // tag as small 61 } 62 return result; 63 } 64 65 ~this() nothrow @trusted @nogc 66 { 67 if (isLarge) 68 { 69 static if (hasElaborateDestructor!Large) 70 .destroy(large); 71 } 72 else 73 { 74 static if (hasElaborateDestructor!K) 75 static assert(0, "Destroy non-null elements"); 76 } 77 } 78 79 @disable this(this); 80 81 @property size_t capacity() const pure nothrow @trusted @nogc 82 { 83 version(D_Coverage) {} else pragma(inline, true); 84 return small._capacityDummy; 85 } 86 87 @property size_t length() const pure nothrow @trusted @nogc 88 { 89 version(LDC) pragma(inline, true); 90 if (isLarge) 91 return large.length; 92 import std.algorithm.searching : count; 93 return small._store[].count!(bin => Large.isOccupiedBin(bin)); 94 } 95 96 InsertionStatus insert(K key) @trusted 97 { 98 if (isLarge) 99 return large.insert(key); 100 else 101 { 102 assert(!key.isNull); 103 104 // try inserting into small 105 foreach (immutable index; 0 .. small.maxCapacity) // TODO: benchmark with `static foreach` 106 { 107 if (small._store[index].isNull) // free slot 108 { 109 move(key, small._store[index]); 110 return InsertionStatus.added; 111 } 112 } 113 114 // not hit 115 expandWithExtraCapacity(1); 116 assert(isLarge); 117 return large.insert(key); 118 } 119 } 120 121 bool remove()(const scope K key) @trusted /* template-lazy */ 122 { 123 assert(!key.isNull); 124 if (isLarge) 125 { 126 const hit = large.remove(key); 127 if (large.length <= small.maxCapacity) 128 shrinkLargeToSmall(); 129 return hit; 130 } 131 else 132 { 133 foreach (immutable index; 0 .. small.maxCapacity) // TODO: benchmark with `static foreach` 134 { 135 if (small._store[index] is key) 136 { 137 small._store[index].nullify(); // don't need holes for small array 138 return true; 139 } 140 } 141 return false; 142 } 143 } 144 145 private void shrinkLargeToSmall()() @trusted /* template-lazy */ 146 { 147 Large largeCopy = void; 148 moveEmplace(large, largeCopy); // TODO: no need to reset `large` 149 150 size_t count = 0; 151 foreach (ref e; largeCopy.rawStore) 152 { 153 if (e.isNull) { continue; } 154 static if (Large.hasAddressLikeKey) 155 if (Large.isHoleKeyConstant(e)) 156 continue; 157 moveEmplace(e, small._store[count]); 158 count += 1; 159 } 160 161 foreach (immutable index; count .. small.maxCapacity) 162 { 163 static if (hasElaborateDestructor!K) 164 emplace(&small._store[index]); // reset 165 small._store[index].nullify(); 166 } 167 } 168 169 /** Check if `element` is stored. 170 Returns: `true` if element is present, `false` otherwise. 171 */ 172 bool contains(const scope K key) const @trusted /* template-lazy */ // `auto ref` here makes things slow 173 in(!key.isNull) 174 { 175 if (isLarge) 176 return large.contains(key); 177 static if (Large.hasAddressLikeKey) 178 assert(!Large.isHoleKeyConstant(key)); 179 // TODO: is static foreach faster here? 180 import std.algorithm.searching : canFind; 181 alias pred = (a, b) => a is b; // TODO: add to template 182 return small._store[].canFind!(pred)(key); 183 } 184 185 private void expandWithExtraCapacity(size_t extraCapacity) @trusted 186 { 187 Small.Bins binsCopy = small._store; // TODO: moveEmplace 188 // TODO: merge these lines? 189 emplace!Large(&large); 190 large.reserveExtra(Small.maxCapacity + extraCapacity); 191 large.insertN(binsCopy); 192 } 193 194 /// Get bin count. 195 @property size_t binCount() const @trusted 196 { 197 pragma(inline, true) 198 if (isLarge) 199 return large.binCount; 200 return small.maxCapacity; 201 } 202 203 @property private scope inout(K)[] bins() inout @trusted return 204 { 205 version(D_Coverage) {} else pragma(inline, true); 206 if (isLarge) 207 return large.rawStore; 208 return small._store[]; 209 } 210 211 private: 212 bool isLarge() const pure nothrow @trusted @nogc 213 { 214 version(D_Coverage) {} else pragma(inline, true); 215 return small._capacityDummy > Small.maxCapacity; 216 } 217 218 /// Returns: `true` if `this` currently uses small (packed) array storage. 219 bool isSmall() const pure nothrow @trusted @nogc { return !isLarge; } 220 221 union 222 { 223 alias Large = HybridHashSet!(K, hasher); 224 Large large; 225 static struct Small 226 { 227 /* discriminator between large and small storage; must be placed at 228 * exactly here (maps to position of `large._store.length`) and 229 * always contain `maxCapacity` when this is small */ 230 size_t _capacityDummy; 231 enum maxCapacity = (large.sizeof - _capacityDummy.sizeof)/K.sizeof; 232 static assert(maxCapacity, "Cannot fit a single element in a Small"); 233 alias Bins = K[maxCapacity]; 234 Bins _store; 235 } 236 Small small; 237 }; 238 } 239 240 /** L-value element reference (and in turn range iterator). 241 */ 242 static private struct LvalueElementRef(Table) 243 { 244 import std.traits : isMutable; 245 debug static assert(isMutable!Table, "Table type should always be mutable"); 246 247 private Table* _table; // scoped access 248 private size_t _binIndex; // index to bin inside `table` 249 private size_t _hitCounter; // counter over number of elements popped (needed for length) 250 251 this(Table* table) @trusted 252 { 253 version(D_Coverage) {} else pragma(inline, true); 254 this._table = table; 255 // static if (Table.isBorrowChecked) 256 // { 257 // debug 258 // { 259 // _table.incBorrowCount(); 260 // } 261 // } 262 } 263 264 ~this() nothrow @trusted @nogc 265 { 266 version(D_Coverage) {} else pragma(inline, true); 267 // static if (Table.isBorrowChecked) 268 // { 269 // debug 270 // { 271 // _table.decBorrowCount(); 272 // } 273 // } 274 } 275 276 this(this) @trusted 277 { 278 version(D_Coverage) {} else pragma(inline, true); 279 // static if (Table.isBorrowChecked) 280 // { 281 // debug 282 // { 283 // assert(_table._borrowCount != 0); 284 // _table.incBorrowCount(); 285 // } 286 // } 287 } 288 289 /// Check if empty. 290 bool empty() const @property @safe pure nothrow @nogc 291 { 292 version(D_Coverage) {} else pragma(inline, true); 293 return _binIndex == _table.binCount; 294 } 295 296 /// Get number of element left to pop. 297 @property size_t length() const @safe pure nothrow @nogc 298 { 299 version(D_Coverage) {} else pragma(inline, true); 300 return _table.length - _hitCounter; 301 } 302 303 @property typeof(this) save() // ForwardRange 304 { 305 version(D_Coverage) {} else pragma(inline, true); 306 return this; 307 } 308 309 void popFront() 310 { 311 version(LDC) pragma(inline, true); 312 assert(!empty); 313 _binIndex += 1; 314 findNextNonEmptyBin(); 315 _hitCounter += 1; 316 } 317 318 private void findNextNonEmptyBin() 319 { 320 version(D_Coverage) {} else pragma(inline, true); 321 while (_binIndex != (*_table).binCount && 322 !Table.Large.isOccupiedBin(_table.bins[_binIndex])) 323 _binIndex += 1; 324 } 325 } 326 327 /// Range over elements of l-value instance of this. 328 static private struct ByLvalueElement(Table) 329 { 330 pragma(inline, true): 331 import std.traits : isMutable; 332 static if (isAddress!(Table.Large.ElementType)) // for reference types 333 { 334 /// Get reference to front element. 335 @property scope Table.Large.ElementType front()() return @trusted 336 { 337 // cast to head-const for class key 338 return (cast(typeof(return))_table.bins[_binIndex]); 339 } 340 } 341 else 342 { 343 /// Get reference to front element. 344 @property scope auto ref front() return @trusted 345 { 346 return *(cast(const(Table.ElementType)*)&_table._store[_binIndex]); // propagate constnes 347 } 348 } 349 import core.internal.traits : Unqual; 350 // unqual to reduce number of instantations of `LvalueElementRef` 351 public LvalueElementRef!(Unqual!Table) _elementRef; 352 alias _elementRef this; 353 } 354 355 /** Returns: range that iterates through the elements of `c` in undefined order. 356 */ 357 auto byElement(Table)(auto ref return Table c) @trusted 358 if (isInstanceOf!(SSOHybridHashSet, Table)) 359 { 360 import core.internal.traits : Unqual; 361 alias M = Unqual!Table; 362 alias C = const(Table); // be const for now 363 static if (__traits(isRef, c)) // `c` is an l-value and must be borrowed 364 auto result = ByLvalueElement!C((LvalueElementRef!(M)(cast(M*)&c))); 365 else // `c` was is an r-value and can be moved 366 static assert(0, "R-value parameter not supported"); 367 result.findNextNonEmptyBin(); 368 return result; 369 } 370 alias range = byElement; // EMSI-container naming 371 372 /// start small and expand to large 373 @safe pure nothrow unittest 374 { 375 import std.algorithm.comparison : equal; 376 import nxt.digestx.fnv : FNV; 377 import nxt.array_help : s; 378 379 // construct small 380 alias X = SSOHybridHashSet!(K, FNV!(64, true)); 381 static assert(X.sizeof == 24); 382 auto x = X.withCapacity(X.small.maxCapacity); 383 assert(x.isSmall); 384 assert(x.capacity == X.small.maxCapacity); 385 assert(x.length == 0); 386 387 auto k42 = new K(42); 388 auto k43 = new K(43); 389 auto k44 = new K(44); 390 391 // insert first into small 392 393 assert(!x.contains(k42)); 394 395 assert(x.insert(k42) == x.InsertionStatus.added); 396 assert(x.contains(k42)); 397 398 assert(x.remove(k42)); 399 assert(!x.contains(k42)); 400 401 assert(x.insert(k42) == x.InsertionStatus.added); 402 assert(x.contains(k42)); 403 404 assert(x.byElement.equal!((a, b) => a is b)([k42].s[])); 405 assert(x.isSmall); 406 assert(x.length == 1); 407 408 // insert second into small 409 410 assert(!x.contains(k43)); 411 412 assert(x.insert(k43) == x.InsertionStatus.added); 413 assert(x.contains(k42)); 414 assert(x.contains(k43)); 415 416 assert(x.remove(k43)); 417 assert(x.remove(k42)); 418 assert(!x.contains(k43)); 419 assert(!x.contains(k42)); 420 421 assert(x.insert(k43) == x.InsertionStatus.added); 422 assert(x.insert(k42) == x.InsertionStatus.added); 423 424 assert(x.byElement.equal!((a, b) => a is b)([k43, k42].s[])); 425 assert(x.isSmall); 426 assert(x.length == 2); 427 428 // expanding insert third into large 429 430 assert(!x.contains(k44)); 431 assert(x.insert(k44) == x.InsertionStatus.added); 432 // unordered store so equal doesn't work anymore 433 assert(x.contains(k42)); 434 assert(x.contains(k43)); 435 assert(x.contains(k44)); 436 foreach (ref e; x.byElement) 437 { 438 static assert(is(typeof(e) == K)); 439 assert(x.contains(e)); 440 } 441 assert(x.isLarge); 442 assert(x.length == 3); 443 444 // shrinking remove third into small 445 assert(x.remove(k44)); 446 assert(!x.contains(k44)); 447 assert(x.isSmall); 448 assert(x.length == 2); 449 assert(x.byElement.equal!((a, b) => a is b)([k43, k42].s[])); 450 // auto xr = x.byElement; 451 // assert(xr.front.value == 43); 452 // assert(xr.front is k43); 453 // xr.popFront(); 454 // assert(xr.front.value == 42); 455 // assert(xr.front is k42); 456 // xr.popFront(); 457 // assert(xr.empty); 458 459 const cx = X.withCapacity(X.small.maxCapacity); 460 foreach (ref e; cx.byElement) 461 static assert(is(typeof(e) == K)); // head-const because of `is`-equality 462 } 463 464 /// start large 465 @safe pure nothrow unittest 466 { 467 import nxt.digestx.fnv : FNV; 468 alias X = SSOHybridHashSet!(K, FNV!(64, true)); 469 import nxt.container.traits : mustAddGCRange; 470 static assert(mustAddGCRange!K); 471 static assert(mustAddGCRange!X); 472 auto x = X.withCapacity(3); 473 assert(x.isLarge); 474 assert(x.capacity == 4); // nextPow2(3) 475 assert(x.length == 0); 476 assert(x.insert(new K(42)) == x.InsertionStatus.added); 477 assert(x.length == 1); 478 assert(x.insert(new K(43)) == x.InsertionStatus.added); 479 assert(x.length == 2); 480 } 481 482 version(unittest) 483 { 484 class K 485 { 486 this(uint value) @safe pure nothrow @nogc 487 { 488 this.value = value; 489 } 490 uint value; 491 } 492 }