1 // Written in the D programming language. 2 3 /** 4 Group and related families for Map 5 6 Copyright: Copyright 2024- Masahiro Nakagawa 7 Authros: Masahiro Nakagawa 8 License: $(HTTP www.apache.org/licenses/LICENSE-2.0, Apache License Version 2.0) 9 */ 10 module swisstable.group; 11 12 import core.bitop : bsr, bsf, bswap; 13 14 nothrow pure @safe @nogc 15 { 16 17 alias H1 = ulong; 18 alias H2 = ubyte; 19 20 /// Extract H1 portion from given hash. This value is used for slot index. 21 pragma(inline, true) H1 calcH1(size_t h) { return (h & 0xFFFF_FFFF_FFFF_FF80) >> 7; } 22 /// Extract H2 portion from given hash. This value is used for Control byte. 23 pragma(inline, true) H2 calcH2(size_t h) { return h & 0x7F; } 24 25 enum Control : byte 26 { 27 Empty = -128, 28 Tombstone = -2, // kDeleted in original implementation 29 Sentinel = -1 30 } 31 32 // Helpers for Control state check 33 pragma(inline, true) bool isEmpty(Control c) { return c == Control.Empty; } 34 pragma(inline, true) bool isFull(Control c) { return c >= 0; } 35 pragma(inline, true) bool isTombstone(Control c) { return c == Control.Tombstone; } 36 pragma(inline, true) bool isEmptyOrTombstone(Control c) { return c < Control.Sentinel; } 37 38 // TODO: Use ldc.intrinsics in LDC. 39 pragma(inline, true) uint countTrailingZeros(T)(T v) 40 { 41 return v ? cast(uint)bsf(v) : cast(uint)(8 * T.sizeof); 42 } 43 44 unittest 45 { 46 assert(countTrailingZeros(0U) == 32); 47 assert(countTrailingZeros(0UL) == 64); 48 assert(countTrailingZeros(0b0010) == 1); 49 assert(countTrailingZeros(0b01000000) == 6); 50 } 51 52 pragma(inline, true) uint countLeadingZeros(T)(T v) 53 { 54 static if (is(T == ubyte)) { 55 return v ? 7 - bsr(v) : 8; 56 } else static if (is(T == ushort)) { 57 return v ? 15 - bsr(v) : 16; 58 } else static if (is(T == uint)) { 59 return v ? 31 - bsr(v) : 32; 60 } else { 61 return v ? 63 - bsr(v) : 64; 62 } 63 } 64 65 unittest 66 { 67 assert(countLeadingZeros(0U) == 32); 68 assert(countLeadingZeros(0UL) == 64); 69 assert(countLeadingZeros(cast(ubyte)0b01000000) == 1); 70 assert(countLeadingZeros(0x0080_0000U) == 8); 71 } 72 73 } // nothrow pure @safe @nogc 74 75 Control* emptyGroup() nothrow @trusted 76 { 77 static Control zero() { return cast(Control)0; } 78 static immutable Control[32] EmptyGroup = [ 79 zero, zero, zero, zero, zero, zero, zero, zero, zero, zero, zero, zero, zero, zero, zero, zero, 80 Control.Sentinel, Control.Empty, Control.Empty, Control.Empty, Control.Empty, Control.Empty, Control.Empty, Control.Empty, 81 Control.Empty, Control.Empty, Control.Empty, Control.Empty, Control.Empty, Control.Empty, Control.Empty, Control.Empty 82 ]; 83 84 return cast(Control*)EmptyGroup.ptr + 16; 85 } 86 87 private enum ulong LSBs = 0x0101010101010101UL; 88 private enum ulong MSBs = 0x8080808080808080UL; 89 90 /** 91 * Iterable Mask for bits operation. This object is used in Map's lookup operation with Group. 92 * 93 * Bits and Shift are depend on Group implementation(SSE2 / Porable). 94 */ 95 struct BitMask(T, int Bits, int Shift = 0) 96 { 97 private: 98 T _mask; 99 100 public nothrow @safe: 101 this(T mask) { _mask = mask; } 102 bool opCast(T : bool)() const { return _mask != 0; } 103 bool empty() const { return _mask == 0; } 104 uint front() const { return (countTrailingZeros(_mask) >> Shift); } 105 void popFront() { _mask &= (_mask - 1); } 106 uint highestBitSet() const { return cast(uint)(bsr(_mask) >> Shift); } 107 uint leadingZeros() const 108 { 109 enum int extraBits = cast(int)((T.sizeof * 8) - (Bits << Shift)); 110 return cast(uint)(countLeadingZeros(cast(T)(_mask << extraBits))) >> Shift; 111 } 112 alias lowestBitSet = front; 113 alias trailingZeros = front; 114 } 115 116 unittest 117 { 118 import std.algorithm : equal; 119 120 alias BitMaskUbyte = BitMask!(ubyte, 8); 121 assert(equal(BitMaskUbyte(0x1), [0])); 122 assert(equal(BitMaskUbyte(0x5), [0, 2])); 123 assert(equal(BitMaskUbyte(0x55), [0, 2, 4, 6])); 124 assert(equal(BitMaskUbyte(0xaa), [1, 3, 5, 7])); 125 126 alias BitMaskUlong = BitMask!(ulong, 8, 3); 127 assert(equal(BitMaskUlong(0x0000000080800000UL), [2, 3])); 128 assert(equal(BitMaskUlong(MSBs), [0, 1, 2, 3, 4, 5, 6, 7])); 129 assert(equal(BitMaskUlong(0x8000808080008000UL), [1, 3, 4, 5, 7])); 130 } 131 132 version (D_SIMD) 133 { 134 135 /** 136 * Process a group of control bytes using SIMD/SSE2 137 */ 138 struct GroupSSE2 139 { 140 import core.simd; 141 142 enum size_t Width = 16; 143 alias BitMaskType = BitMask!(ushort, Width); 144 145 private: 146 byte16 ctrl; 147 148 public nothrow @trusted: 149 this(in Control* c) 150 { 151 ctrl = loadUnaligned!(byte16)(cast(const byte16*)c); 152 } 153 154 /// Returns: a BitMask representing the positions of `hash` matched slots 155 pragma(inline, true) BitMaskType match(in H2 hash) const 156 { 157 auto m = mm_set1_epi8(hash); 158 return BitMaskType(cast(ushort)mm_movemask_epi8(cast(byte16)__simd(XMM.PCMPEQB, m, ctrl))); 159 } 160 161 /// Returns: a BitMask representing the positions of empty slots 162 pragma(inline, true) BitMaskType maskEmpty() const 163 { 164 return BitMaskType(cast(ushort)mm_movemask_epi8(cast(byte16)__simd(XMM.PSIGNB, ctrl, ctrl))); 165 } 166 167 /// Returns: a BitMask representing the positions of full slots 168 BitMaskType maskFull() const 169 { 170 return BitMaskType(cast(ushort)mm_movemask_epi8(ctrl) ^ 0xffff); 171 } 172 173 /// Returns: a BitMask representing the positions of non-full slots 174 BitMaskType maskNonFull() const 175 { 176 return BitMaskType(cast(ushort)mm_movemask_epi8(ctrl)); 177 } 178 179 /// Returns: a BitMask representing the positions of empty or tombstone slots 180 BitMaskType maskEmptyOrTombstone() const 181 { 182 auto m = mm_set1_epi8(Control.Sentinel); 183 return BitMaskType(cast(ushort)mm_movemask_epi8(cast(byte16)__simd(XMM.PCMPGTB, m, ctrl))); 184 } 185 186 /// Returns: the number of trailing empty or tombstone slots in the group. 187 uint countLeadingEmptyOrTombstone() const 188 { 189 auto m = mm_set1_epi8(Control.Sentinel); 190 return countTrailingZeros(cast(uint)mm_movemask_epi8(cast(byte16)__simd(XMM.PCMPGTB, m, ctrl)) + 1); 191 } 192 193 /// Convert empty/tombstone to empty and full to tombstone in the group.. 194 void convertSpecialToEmptyAndFullToTombstone(Control* dst) const 195 { 196 const msbs = mm_set1_epi8(-128); 197 const x126 = mm_set1_epi8(126); 198 auto res = cast(byte16)__simd(XMM.POR, __simd(XMM.PSHUFB, x126, ctrl), msbs); 199 storeUnaligned!(byte16)(cast(byte16*)dst, res); 200 } 201 202 private: 203 pragma(inline, true) byte16 mm_set1_epi8(in byte v) pure const 204 { 205 return byte16(v); 206 } 207 208 // core.simd doesn't support XMM.PMOVMSKB 209 pragma(inline, true) int mm_movemask_epi8(in byte16 v) pure const 210 { 211 asm nothrow pure 212 { 213 naked; 214 pmovmskb EAX, XMM0; 215 ret; 216 } 217 } 218 } 219 220 unittest 221 { 222 import std.algorithm : equal; 223 224 Control[16] group = cast(Control[16])[ 225 Control.Empty, 1, Control.Tombstone, 3, Control.Empty, 5, Control.Sentinel, 7, 226 7, 5, 3, 1, 1, 1, 1, 1]; 227 228 auto g = GroupSSE2(group.ptr); 229 // match 230 assert(g.match(0).empty); 231 assert(equal(g.match(1), [1, 11, 12, 13, 14, 15])); 232 assert(equal(g.match(3), [3, 10])); 233 assert(equal(g.match(5), [5, 9])); 234 assert(equal(g.match(7), [7, 8])); 235 // maskEmpty 236 assert(g.maskEmpty.lowestBitSet == 0); 237 assert(g.maskEmpty.highestBitSet == 4); 238 // maskEmptyOrTombstone 239 assert(g.maskEmptyOrTombstone.lowestBitSet == 0); 240 assert(g.maskEmptyOrTombstone.highestBitSet == 4); 241 } 242 243 unittest 244 { 245 // GroupSSE2 maskFull / maskNonFull 246 import std.algorithm : equal; 247 248 Control[16] group = cast(Control[16])[ 249 Control.Empty, 1, Control.Tombstone, 3, Control.Empty, 5, Control.Sentinel, 7, 250 7, 5, Control.Tombstone, 1, 1, Control.Sentinel, Control.Empty, 1]; 251 252 auto g = GroupSSE2(group.ptr); 253 assert(equal(g.maskFull, [1, 3, 5, 7, 8, 9, 11, 12, 15])); 254 assert(equal(g.maskNonFull, [0, 2, 4, 6, 10, 13, 14])); 255 } 256 257 } // D_SIMD 258 259 /** 260 * Process a group of control bytes using portable bit operation 261 */ 262 struct GroupPortable 263 { 264 enum size_t Width = 8; 265 266 alias BitMaskType = BitMask!(ulong, Width, 3); 267 268 private: 269 ulong ctrl; 270 271 public nothrow @safe: 272 this(in Control* ctrl) 273 { 274 this.ctrl = load64bits(ctrl); 275 } 276 277 pragma(inline, true) BitMaskType match(H2 hash) const 278 { 279 auto x = ctrl ^ (LSBs * hash); 280 return BitMaskType((x - LSBs) & ~x & MSBs); 281 } 282 283 pragma(inline, true) BitMaskType maskEmpty() const 284 { 285 return BitMaskType((ctrl & ~(ctrl << 6)) & MSBs); 286 } 287 288 BitMaskType maskFull() const 289 { 290 return BitMaskType((ctrl ^ MSBs) & MSBs); 291 } 292 293 BitMaskType maskNonFull() const 294 { 295 return BitMaskType(ctrl & MSBs); 296 } 297 298 BitMaskType maskEmptyOrTombstone() const 299 { 300 return BitMaskType((ctrl & ~(ctrl << 7)) & MSBs); 301 } 302 303 uint countLeadingEmptyOrTombstone() const 304 { 305 return cast(uint)(countTrailingZeros((ctrl | ~(ctrl >> 7)) & LSBs) >> 3); 306 } 307 308 void convertSpecialToEmptyAndFullToTombstone(Control* dst) const 309 { 310 auto x = ctrl & MSBs; 311 store64bits(dst, (~x + (x >> 7)) & ~LSBs); 312 } 313 } 314 315 version (D_SIMD) 316 alias Group = GroupSSE2; 317 else 318 alias Group = GroupPortable; 319 320 unittest 321 { 322 import std.algorithm : equal; 323 324 Control[8] group = cast(Control[8])[Control.Empty, 1, 2, Control.Tombstone, 2, 1, Control.Sentinel, 1]; 325 326 // match 327 assert(GroupPortable(group.ptr).match(0).empty); 328 assert(equal(GroupPortable(group.ptr).match(1), [1, 5, 7])); 329 assert(equal(GroupPortable(group.ptr).match(2), [2, 4])); 330 331 // maskEmpty 332 assert(GroupPortable(group.ptr).maskEmpty().lowestBitSet == 0); 333 assert(GroupPortable(group.ptr).maskEmpty().highestBitSet == 0); 334 335 // maskEmptyOrTombstone 336 assert(GroupPortable(group.ptr).maskEmptyOrTombstone().lowestBitSet == 0); 337 assert(GroupPortable(group.ptr).maskEmptyOrTombstone().highestBitSet == 3); 338 339 Control[8] group2 = cast(Control[8])[Control.Empty, 1, Control.Empty, Control.Tombstone, 2, Control.Sentinel, Control.Sentinel, 1]; 340 341 // maskFull / maskNonFull 342 assert(equal(GroupPortable(group2.ptr).maskFull(), [1, 4, 7])); 343 assert(equal(GroupPortable(group2.ptr).maskNonFull(), [0, 2, 3, 5, 6])); 344 } 345 346 unittest 347 { 348 import std.meta; 349 350 version (D_SIMD) 351 alias TestTypes = AliasSeq!(GroupPortable, GroupSSE2); 352 else 353 alias TestTypes = AliasSeq!(GroupPortable); 354 355 // Group's convertSpecialToEmptyAndFullToTombstone test for dropDeletesWithoutResize() 356 foreach (TestGroup; TestTypes) { 357 enum size_t Cap = 63; 358 enum size_t GW = TestGroup.Width; 359 360 Control[] ctrl = new Control[](Cap + 1 + GW); 361 ctrl[Cap] = Control.Sentinel; 362 Control[] pattern = cast(Control[])[Control.Empty, 2, Control.Tombstone, 2, Control.Empty, 1, Control.Tombstone]; 363 for (size_t i; i != Cap; i++) { 364 ctrl[i] = pattern[i % pattern.length]; 365 if (i < GW - 1) 366 ctrl[i + Cap + 1] = pattern[i % pattern.length]; 367 } 368 Control* ctrlPtr = ctrl.ptr; 369 for (Control* pos = ctrlPtr; pos < ctrlPtr + Cap; pos += GW) 370 TestGroup(pos).convertSpecialToEmptyAndFullToTombstone(pos); 371 memcpy(ctrlPtr + Cap + 1, ctrlPtr, GW - 1); 372 ctrl[Cap] = Control.Sentinel; 373 374 for (size_t i; i < Cap + GW; i++) { 375 Control expected = pattern[i % (Cap + 1) % pattern.length]; 376 if (i == Cap) 377 expected = Control.Sentinel; 378 if (expected == Control.Tombstone) 379 expected = Control.Empty; 380 if (expected.isFull) 381 expected = Control.Tombstone; 382 assert(ctrl[i] == expected); 383 } 384 } 385 } 386 387 unittest 388 { 389 import std.meta : AliasSeq; 390 import std.array : array; 391 import std.range : repeat; 392 393 version (D_SIMD) 394 alias TestTypes = AliasSeq!(GroupPortable, GroupSSE2); 395 else 396 alias TestTypes = AliasSeq!(GroupPortable); 397 398 // Group's countLeadingEmptyOrTombstone test 399 foreach (TestGroup; TestTypes) { 400 Control[] empties = [Control.Empty, Control.Tombstone]; 401 Control[] fulls = cast(Control[])[0, 1, 2, 3, 5, 9, 126, Control.Sentinel]; 402 403 foreach (empty; empties) { 404 Control[] e = empty.repeat(TestGroup.Width).array; 405 assert(TestGroup.Width == TestGroup(e.ptr).countLeadingEmptyOrTombstone()); 406 407 foreach (full; fulls) { 408 foreach (i; 0..TestGroup.Width) { 409 Control[] f = empty.repeat(TestGroup.Width).array; 410 f[i] = full; 411 assert(i == TestGroup(f.ptr).countLeadingEmptyOrTombstone); 412 } 413 Control[] f = empty.repeat(TestGroup.Width).array; 414 f[TestGroup.Width * 2 / 3] = full; 415 f[TestGroup.Width / 2] = full; 416 assert((TestGroup.Width / 2) == TestGroup(f.ptr).countLeadingEmptyOrTombstone); 417 } 418 } 419 } 420 } 421 422 import core.stdc.string : memcpy; 423 424 nothrow @trusted @nogc: 425 426 ulong load64bits(in Control* src) 427 { 428 ulong r; 429 memcpy(&r, src, ulong.sizeof); 430 431 version (LittleEndian) 432 { 433 return r; 434 } 435 else 436 { 437 return bswap(r); 438 } 439 } 440 441 void store64bits(Control* dst, ulong src) 442 { 443 version (LittleEndian) 444 { 445 memcpy(dst, &src, ulong.sizeof); 446 } 447 else 448 { 449 ulong v = bswap(src); 450 memcpy(dst, &v, ulong.sizeof); 451 } 452 }